From: David ‘Bombe’ Roden Date: Thu, 10 Jan 2013 18:04:51 +0000 (+0100) Subject: Rename project to “Rhynodge.” X-Git-Tag: 0.1~40 X-Git-Url: https://git.pterodactylus.net/?p=rhynodge.git;a=commitdiff_plain;h=6f69aff66ba5617d0bb27874014b4274bc551ab8 Rename project to “Rhynodge.” --- diff --git a/README.md b/README.md index a17c584..1a01145 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# Reactor +# Rhynodge ## Description -Reactor is a tool that lets you periodically execute tasks that depend on certain conditions. +Rhynodge is a tool that lets you periodically execute tasks that depend on certain conditions. Its concept is quite similar to websites like ifttt (“if this then that”): you evaluate an input condition (e. g. data from a website, Facebook or Twitter posts, incoming emails, existence of a file), and if it evaluates to “yes” you execute a certain action. ## Concepts -The core of Reactor comprises ``Reaction``s which in turn consist of ``Query``s, ``Filter``s, ``Trigger``s, and ``Action``s. +The core of Rhynodge comprises ``Reaction``s which in turn consist of ``Query``s, ``Filter``s, ``Trigger``s, and ``Action``s. ### Query @@ -34,4 +34,4 @@ If a trigger found a change, the action is then executed. Again, an action can b ## Configuration -Reactor’s configuration uses JSON files (I tried using XML first but apparently polymorphic deserialization is something that is not easily done with XML parsers). The format of a ``Chain`` configuration is pretty straight-forward and can be seen in the example configuration files. +Rhynodge’s configuration uses JSON files (I tried using XML first but apparently polymorphic deserialization is something that is not easily done with XML parsers). The format of a ``Chain`` configuration is pretty straight-forward and can be seen in the example configuration files. diff --git a/pom.xml b/pom.xml index 0130d8b..df78686 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 net.pterodactylus - reactor + rhynodge 0.0.1-SNAPSHOT UTF-8 @@ -22,7 +22,7 @@ exec-maven-plugin 1.2.1 - net.pterodactylus.reactor.engine.Starter + net.pterodactylus.rhynodge.engine.Starter diff --git a/src/main/java/net/pterodactylus/reactor/Action.java b/src/main/java/net/pterodactylus/reactor/Action.java deleted file mode 100644 index 650c3ad..0000000 --- a/src/main/java/net/pterodactylus/reactor/Action.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Reactor - Action.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public interface Action { - - /** - * Performs the action. - * - * @param output - * The output for the action - */ - void execute(Output output); - -} diff --git a/src/main/java/net/pterodactylus/reactor/Filter.java b/src/main/java/net/pterodactylus/reactor/Filter.java deleted file mode 100644 index 0afb226..0000000 --- a/src/main/java/net/pterodactylus/reactor/Filter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Reactor - Filter.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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. - *

- * 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 David ‘Bombe’ Roden - */ -public interface Filter { - - /** - * Converts the given state into a different state. - * - * @param state - * The state to convert - * @return The new state - */ - State filter(State state); - -} diff --git a/src/main/java/net/pterodactylus/reactor/Query.java b/src/main/java/net/pterodactylus/reactor/Query.java deleted file mode 100644 index 0b46a39..0000000 --- a/src/main/java/net/pterodactylus/reactor/Query.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Reactor - Query.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor; - -/** - * A query is used to retrieve the current {@link State} of a system. - * - * @author David ‘Bombe’ Roden - */ -public interface Query { - - /** - * Retrieves the current state of the system. The returned state is never - * {@code null}. - * - * @return The current state of the system. - */ - public State state(); - -} diff --git a/src/main/java/net/pterodactylus/reactor/Reaction.java b/src/main/java/net/pterodactylus/reactor/Reaction.java deleted file mode 100644 index 65a637b..0000000 --- a/src/main/java/net/pterodactylus/reactor/Reaction.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Reactor - Reaction.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 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. 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 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 filters() { - return filters; - } - - /** - * Returns the trigger to detect changes. - * - * @return The trigger to detect changes - */ - public Trigger trigger() { - return trigger; - } - - /** - * Returns the action to perform. - * - * @return The action to perform - */ - public Action action() { - return action; - } - - /** - * Returns the update interval of this reaction. - * - * @return The update interval of this reaction (in milliseconds) - */ - public long updateInterval() { - return updateInterval; - } - - /** - * Sets the update interval of this reaction. - * - * @param updateInterval - * The update interval of this reaction (in milliseconds) - * @return This reaction - */ - public Reaction setUpdateInterval(long updateInterval) { - this.updateInterval = updateInterval; - return this; - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/State.java b/src/main/java/net/pterodactylus/reactor/State.java deleted file mode 100644 index 278032e..0000000 --- a/src/main/java/net/pterodactylus/reactor/State.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Reactor - State.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor; - -/** - * Defines the current state of a system. - * - * @author David ‘Bombe’ Roden - */ -public interface State { - - /** - * Returns the time when this state was retrieved. - * - * @return The time when this state was retrieved (in millseconds since Jan - * 1, 1970 UTC) - */ - long time(); - - /** - * Whether the state was successfully retrieved. This method should only - * return {@code true} if a meaningful result could be retrieved; if e. g. a - * service is currently not reachable, this method should return false - * instead of emulating success by using empty lists or similar constructs. - * - * @return {@code true} if the state could be retrieved successfully, - * {@code false} otherwise - */ - boolean success(); - - /** - * Returns the number of consecutive failures. This method only returns a - * meaningful number iff {@link #success()} returns {@code false}. If - * {@link #success()} returns {@code false} for the first time after - * returning {@code true} and this method is called after {@link #success()} - * it will return {@code 1}. - * - * @return The number of consecutive failures - */ - int failCount(); - - /** - * Sets the fail count of this state. - * - * @param failCount - * The fail count of this state - */ - void setFailCount(int failCount); - - /** - * If {@link #success()} returns {@code false}, this method may return a - * {@link Throwable} to give some details for the reason why retrieving the - * state was not possible. For example, network-based {@link Query}s might - * return any exception that were encountered while communicating with the - * remote service. - * - * @return An exception that occured, may be {@code null} in case an - * exception can not be meaningfully returned - */ - Throwable exception(); - -} diff --git a/src/main/java/net/pterodactylus/reactor/Trigger.java b/src/main/java/net/pterodactylus/reactor/Trigger.java deleted file mode 100644 index 277eeb6..0000000 --- a/src/main/java/net/pterodactylus/reactor/Trigger.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Reactor - Trigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public interface Trigger { - - /** - * Checks whether the given states warrant a change trigger. - * - * @param currentState - * The current state of a system - * @param previousState - * The previous state of the system - * @return {@code true} if the given states warrant a change trigger, - * {@code false} otherwise - */ - boolean triggers(State currentState, State previousState); - - /** - * Returns the output of this trigger. This will only return a meaningful - * value if {@link #triggers(State, State)} returns {@code true}. - * - * @param reaction - * The reaction being triggered - * @return The output of this trigger - */ - Output output(Reaction reaction); - -} diff --git a/src/main/java/net/pterodactylus/reactor/actions/EmailAction.java b/src/main/java/net/pterodactylus/reactor/actions/EmailAction.java deleted file mode 100644 index ef9d602..0000000 --- a/src/main/java/net/pterodactylus/reactor/actions/EmailAction.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Reactor - EmailAction.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class EmailAction implements Action { - - /** The name of the SMTP host. */ - private final String hostname; - - /** The email address of the sender. */ - private final String sender; - - /** The email address of the recipient. */ - private final String recipient; - - /** - * Creates a new email action. - * - * @param hostname - * The hostname of the SMTP server - * @param sender - * The email address of the sender - * @param recipient - * The email address of the recipient - */ - public EmailAction(String hostname, String sender, String recipient) { - this.hostname = hostname; - this.sender = sender; - this.recipient = recipient; - } - - // - // ACTION METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public void execute(Output output) { - Properties properties = System.getProperties(); - properties.put("mail.smtp.host", hostname); - Session session = Session.getInstance(properties); - MimeMessage message = new MimeMessage(session); - try { - /* create message. */ - message.setFrom(new InternetAddress(sender)); - message.setRecipient(RecipientType.TO, new InternetAddress(recipient)); - message.setSubject(output.summary()); - - /* create text and html parts. */ - MimeMultipart multipart = new MimeMultipart(); - multipart.setSubType("alternative"); - MimeBodyPart textPart = new MimeBodyPart(); - textPart.setContent(output.text("text/plain", -1), "text/plain;charset=utf-8"); - MimeBodyPart htmlPart = new MimeBodyPart(); - htmlPart.setContent(output.text("text/html", -1), "text/html;charset=utf-8"); - multipart.addBodyPart(textPart); - multipart.addBodyPart(htmlPart); - message.setContent(multipart); - - Transport.send(message); - } catch (MessagingException me1) { - /* swallow. */ - } - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java b/src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java deleted file mode 100644 index 8cad092..0000000 --- a/src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Reactor - StandardOutAction.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class StandardOutAction implements Action { - - /** - * {@inheritDoc} - */ - @Override - public void execute(Output output) { - System.out.println(String.format("Triggered by %s.", output.text("text/plain", -1))); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/engine/Engine.java b/src/main/java/net/pterodactylus/reactor/engine/Engine.java deleted file mode 100644 index 6f4376b..0000000 --- a/src/main/java/net/pterodactylus/reactor/engine/Engine.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Reactor - Engine.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 reactions = new HashMap(); - - /** - * 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> nextReactions = Maps.newTreeMap(); - String reactionName; - Reaction nextReaction; - synchronized (reactions) { - for (Entry reactionEntry : reactions.entrySet()) { - net.pterodactylus.reactor.State state = stateManager.loadLastState(reactionEntry.getKey()); - long stateTime = (state != null) ? state.time() : 0; - nextReactions.put(stateTime + reactionEntry.getValue().updateInterval(), Pair.of(reactionEntry.getKey(), reactionEntry.getValue())); - } - reactionName = nextReactions.get(nextReactions.firstKey()).getLeft(); - nextReaction = nextReactions.get(nextReactions.firstKey()).getRight(); - } - logger.debug(String.format("Next Reaction: %s.", reactionName)); - - /* wait until the next reaction has to run. */ - net.pterodactylus.reactor.State lastState = stateManager.loadLastState(reactionName); - long lastStateTime = (lastState != null) ? lastState.time() : 0; - int lastStateFailCount = (lastState != null) ? lastState.failCount() : 0; - long waitTime = (lastStateTime + nextReaction.updateInterval()) - System.currentTimeMillis(); - logger.debug(String.format("Time to wait for next Reaction: %d millseconds.", waitTime)); - if (waitTime > 0) { - synchronized (reactions) { - try { - logger.info(String.format("Waiting until %tc.", lastStateTime + nextReaction.updateInterval())); - reactions.wait(waitTime); - } catch (InterruptedException ie1) { - /* we’re looping! */ - } - } - - /* re-start loop to check for new reactions. */ - continue; - } - - /* run reaction. */ - logger.info(String.format("Running Query for %s...", reactionName)); - Query query = nextReaction.query(); - net.pterodactylus.reactor.State state; - try { - logger.debug("Querying system..."); - state = query.state(); - if (state == null) { - state = FailedState.INSTANCE; - } - logger.debug("System queried."); - } catch (Throwable t1) { - logger.warn("Querying system failed!", t1); - state = new AbstractState(t1) { - /* no further state. */ - }; - } - logger.debug(String.format("State is %s.", state)); - - /* convert states. */ - for (Filter filter : nextReaction.filters()) { - if (state.success()) { - net.pterodactylus.reactor.State newState = filter.filter(state); - logger.debug(String.format("Old state is %s, new state is %s.", state, newState)); - state = newState; - } - } - if (!state.success()) { - state.setFailCount(lastStateFailCount + 1); - } - net.pterodactylus.reactor.State lastSuccessfulState = stateManager.loadLastSuccessfulState(reactionName); - stateManager.saveState(reactionName, state); - - /* only run trigger if we have collected two successful states. */ - Trigger trigger = nextReaction.trigger(); - boolean triggerHit = false; - if ((lastSuccessfulState != null) && lastSuccessfulState.success() && state.success()) { - logger.debug("Checking Trigger for changes..."); - triggerHit = trigger.triggers(state, lastSuccessfulState); - } - - /* run action if trigger was hit. */ - logger.debug(String.format("Trigger was hit: %s.", triggerHit)); - if (triggerHit) { - logger.info("Executing Action..."); - nextReaction.action().execute(trigger.output(nextReaction)); - } - - } - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/engine/Starter.java b/src/main/java/net/pterodactylus/reactor/engine/Starter.java deleted file mode 100644 index 0a0941e..0000000 --- a/src/main/java/net/pterodactylus/reactor/engine/Starter.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Reactor - Starter.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 David ‘Bombe’ Roden - */ - private static interface Parameters { - - /** - * Returns the directory to watch for chains. - * - * @return The chain directory - */ - @Option(defaultValue = "chains", shortName = "c", description = "The directory to watch for chains") - String getChainDirectory(); - - /** - * Returns the directory to store states in. - * - * @return The states directory - */ - @Option(defaultValue = "states", shortName = "s", description = "The directory to store states in") - String getStateDirectory(); - - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java b/src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java deleted file mode 100644 index 3f3b6a7..0000000 --- a/src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Reactor - EpisodeFilter.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 episodes = new LinkedHashMap(); - for (TorrentFile torrentFile : torrentState) { - Episode episode = extractEpisode(torrentFile); - if (episode == null) { - continue; - } - episodes.put(episode, episode); - episode = episodes.get(episode); - episode.addTorrentFile(torrentFile); - } - - return new EpisodeState(episodes.values()); - } - - // - // STATIC METHODS - // - - /** - * Extracts episode information from the given torrent file. - * - * @param torrentFile - * The torrent file to extract the episode information from - * @return The extracted episode information, or {@code null} if no episode - * information could be found - */ - private static Episode extractEpisode(TorrentFile torrentFile) { - Matcher matcher = episodePattern.matcher(torrentFile.name()); - if (!matcher.find()) { - return null; - } - String seasonString = matcher.group(1); - String episodeString = matcher.group(2); - if ((seasonString == null) && (episodeString == null)) { - seasonString = matcher.group(3); - episodeString = matcher.group(4); - } - int season = Integer.valueOf(seasonString); - int episode = Integer.valueOf(episodeString); - return new Episode(season, episode); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java b/src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java deleted file mode 100644 index 83b961d..0000000 --- a/src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Reactor - HtmlFilter.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class HtmlFilter implements Filter { - - /** - * {@inheritDoc} - */ - @Override - public State filter(State state) { - if (!state.success()) { - return FailedState.from(state); - } - checkState(state instanceof HttpState, "state is not a HttpState but a %s", state.getClass().getName()); - Document document = Jsoup.parse(((HttpState) state).content(), ((HttpState) state).uri()); - return new HtmlState(((HttpState) state).uri(), document); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java b/src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java deleted file mode 100644 index 9241ce7..0000000 --- a/src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Reactor - KickAssTorrentsFilter.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class KickAssTorrentsFilter implements Filter { - - /** - * {@inheritDoc} - */ - @Override - public State filter(State state) { - if (!state.success()) { - return FailedState.from(state); - } - checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName()); - - /* get result table. */ - Document document = ((HtmlState) state).document(); - Elements mainTable = document.select("table.data"); - if (mainTable.isEmpty()) { - /* no main table? */ - return new FailedState(); - } - - /* iterate over all rows. */ - TorrentState torrentState = new TorrentState(); - Elements dataRows = mainTable.select("tr:gt(0)"); - for (Element dataRow : dataRows) { - String name = extractName(dataRow); - String size = extractSize(dataRow); - String magnetUri = extractMagnetUri(dataRow); - String downloadUri; - int fileCount = extractFileCount(dataRow); - int seedCount = extractSeedCount(dataRow); - int leechCount = extractLeechCount(dataRow); - try { - downloadUri = new URI(((HtmlState) state).uri()).resolve(extractDownloadUri(dataRow)).toString(); - TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount); - torrentState.addTorrentFile(torrentFile); - } catch (URISyntaxException use1) { - /* ignore; if uri was wrong, we wouldn’t be here. */ - } - } - - return torrentState; - } - - // - // STATIC METHODS - // - - /** - * Extracts the name from the given row. - * - * @param dataRow - * The row to extract the name from - * @return The extracted name - */ - private static String extractName(Element dataRow) { - return dataRow.select("div.torrentname a.normalgrey").text(); - } - - /** - * Extracts the size from the given row. - * - * @param dataRow - * The row to extract the size from - * @return The extracted size - */ - private static String extractSize(Element dataRow) { - return dataRow.select("td:eq(1)").text(); - } - - /** - * Extracts the magnet URI from the given row. - * - * @param dataRow - * The row to extract the magnet URI from - * @return The extracted magnet URI - */ - private static String extractMagnetUri(Element dataRow) { - return dataRow.select("a.imagnet").attr("href"); - } - - /** - * Extracts the download URI from the given row. - * - * @param dataRow - * The row to extract the download URI from - * @return The extracted download URI - */ - private static String extractDownloadUri(Element dataRow) { - return dataRow.select("a.idownload:not(.partner1Button)").attr("href"); - } - - /** - * Extracts the file count from the given row. - * - * @param dataRow - * The row to extract the file count from - * @return The extracted file count - */ - private static int extractFileCount(Element dataRow) { - return Integer.valueOf(dataRow.select("td:eq(2)").text()); - } - - /** - * Extracts the seed count from the given row. - * - * @param dataRow - * The row to extract the seed count from - * @return The extracted seed count - */ - private static int extractSeedCount(Element dataRow) { - return Integer.valueOf(dataRow.select("td:eq(4)").text()); - } - - /** - * Extracts the leech count from the given row. - * - * @param dataRow - * The row to extract the leech count from - * @return The extracted leech count - */ - private static int extractLeechCount(Element dataRow) { - return Integer.valueOf(dataRow.select("td:eq(5)").text()); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/loader/Chain.java b/src/main/java/net/pterodactylus/reactor/loader/Chain.java deleted file mode 100644 index 8c98e50..0000000 --- a/src/main/java/net/pterodactylus/reactor/loader/Chain.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Reactor - Chain.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor.loader; - -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Model for chain definitions. - * - * @author David ‘Bombe’ Roden - */ -public class Chain { - - /** - * Parameter model. - * - * @author David ‘Bombe’ Roden - */ - 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 David ‘Bombe’ Roden - */ - public static class Part { - - /** The class name of the part. */ - @JsonProperty(value = "class") - private String name; - - /** The parameters of the part. */ - @JsonProperty - private List parameters = new ArrayList(); - - /** - * 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 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 filters = new ArrayList(); - - /** 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 filters() { - return filters; - } - - /** - * Returns the trigger of this chain. - * - * @return The trigger of this chain - */ - public Part trigger() { - return trigger; - } - - /** - * Returns the action of this chain. - * - * @return The action of this chain - */ - public Part action() { - return action; - } - - /** - * Returns the update interval of the chain. - * - * @return The update interval (in seconds) - */ - public int updateInterval() { - return updateInterval; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - int hashCode = 0; - hashCode ^= name.hashCode(); - hashCode ^= query.hashCode(); - for (Part filter : filters) { - hashCode ^= filter.hashCode(); - } - hashCode ^= trigger.hashCode(); - hashCode ^= action.hashCode(); - hashCode ^= updateInterval; - return hashCode; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object object) { - if (!(object instanceof Chain)) { - return false; - } - Chain chain = (Chain) object; - if (!name.equals(chain.name)) { - return false; - } - if (!query.equals(chain.query)) { - return false; - } - if (filters.size() != chain.filters.size()) { - return false; - } - for (int filterIndex = 0; filterIndex < filters.size(); ++filterIndex) { - if (!filters.get(filterIndex).equals(chain.filters.get(filterIndex))) { - return false; - } - } - if (!trigger.equals(chain.trigger)) { - return false; - } - if (!action.equals(chain.action)) { - return false; - } - if (updateInterval != chain.updateInterval) { - return false; - } - return true; - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java b/src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java deleted file mode 100644 index 3d30f26..0000000 --- a/src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Reactor - ChainWatcher.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 loadedChains = new HashMap(); - - 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 chains = new HashMap(); - 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 enabledChains = Maps.filterEntries(chains, new Predicate>() { - - @Override - public boolean apply(Entry chainEntry) { - return chainEntry.getValue().enabled(); - } - }); - logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size())); - - /* check for removed chains. */ - Set chainsToRemove = new HashSet(); - for (Entry 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 enabledChain : enabledChains.entrySet()) { - - /* skip already loaded chains. */ - if (loadedChains.containsValue(enabledChain.getValue())) { - continue; - } - - logger.info(String.format("Loading new Chain: %s", enabledChain.getKey())); - - Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue()); - engine.addReaction(enabledChain.getKey(), reaction); - loadedChains.put(enabledChain.getKey(), enabledChain.getValue()); - } - - /* wait before checking again. */ - Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); - } - } - - // - // STATIC METHODS - // - - /** - * Parses the given configuration file into a {@link Chain}. - * - * @param configurationFile - * The configuration file to parse - * @return The parsed chain - */ - private static Chain parseConfigurationFile(File configurationFile) { - try { - return objectMapper.readValue(configurationFile, Chain.class); - } catch (JsonParseException jpe1) { - logger.warn(String.format("Could not parse %s.", configurationFile), jpe1); - } catch (JsonMappingException jme1) { - logger.warn(String.format("Could not parse %s.", configurationFile), jme1); - } catch (IOException ioe1) { - logger.info(String.format("Could not read %s.", configurationFile)); - } - return null; - } - - /** - * Extracts the name of the reaction from the given filename. - * - * @param filename - * The filename to extract the reaction name from - * @return The name of the reaction - */ - private static String getReactionName(String filename) { - return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename; - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/loader/LoaderException.java b/src/main/java/net/pterodactylus/reactor/loader/LoaderException.java deleted file mode 100644 index bffff4a..0000000 --- a/src/main/java/net/pterodactylus/reactor/loader/LoaderException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Reactor - LoaderException.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor.loader; - -/** - * Exception that signals a problem when loading chain XML files. - * - * @author David ‘Bombe’ Roden - */ -public class LoaderException extends Exception { - - /** - * Creates a new loader exception. - */ - public LoaderException() { - super(); - } - - /** - * Creates a new loader exception. - * - * @param message - * The message of the exception - */ - public LoaderException(String message) { - super(message); - } - - /** - * Creates a new loader exception. - * - * @param throwable - * The root cause - */ - public LoaderException(Throwable throwable) { - super(throwable); - } - - /** - * Creates a new loader exception. - * - * @param message - * The message of the exception - * @param throwable - * The root cause - */ - public LoaderException(String message, Throwable throwable) { - super(message, throwable); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java b/src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java deleted file mode 100644 index a2c29aa..0000000 --- a/src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Reactor - Loader.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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 filters = new ArrayList(); - for (Part filterPart : chain.filters()) { - filters.add(ReactionLoader. 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 extractParameters(List parameters) { - List parameterValues = new ArrayList(); - - for (Parameter parameter : parameters) { - parameterValues.add(parameter.value()); - } - - return parameterValues; - } - - /** - * Creates a new object. - *

- * 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. - *

- * 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 createObject(String className, String packageName, List parameters) throws LoaderException { - - /* try to load class without package name. */ - Class objectClass = null; - try { - objectClass = Class.forName(className); - } catch (ClassNotFoundException cnfe1) { - /* ignore, we’ll try again. */ - } - - if (objectClass == null) { - try { - objectClass = Class.forName(packageName + "." + className); - } catch (ClassNotFoundException cnfe1) { - /* okay, now we need to throw. */ - throw new LoaderException(String.format("Could find neither class “%s” nor class “%s.”", className, packageName + "." + className), cnfe1); - } - } - - /* locate an eligible constructor. */ - Constructor wantedConstructor = null; - for (Constructor constructor : objectClass.getConstructors()) { - Class[] parameterTypes = constructor.getParameterTypes(); - if (parameterTypes.length != parameters.size()) { - continue; - } - boolean compatibleTypes = true; - for (Class parameterType : parameterTypes) { - if (parameterType != String.class) { - compatibleTypes = false; - break; - } - } - if (!compatibleTypes) { - continue; - } - wantedConstructor = constructor; - } - - if (wantedConstructor == null) { - throw new LoaderException("Could not find eligible constructor."); - } - - try { - return (T) wantedConstructor.newInstance(parameters.toArray()); - } catch (IllegalArgumentException iae1) { - throw new LoaderException("Could not invoke constructor.", iae1); - } catch (InstantiationException ie1) { - throw new LoaderException("Could not invoke constructor.", ie1); - } catch (IllegalAccessException iae1) { - throw new LoaderException("Could not invoke constructor.", iae1); - } catch (InvocationTargetException ite1) { - throw new LoaderException("Could not invoke constructor.", ite1); - } - - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java b/src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java deleted file mode 100644 index 36d836c..0000000 --- a/src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Reactor - DefaultOutput.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class DefaultOutput implements Output { - - /** The summary of the output. */ - private final String summary; - - /** The texts for the different MIME types. */ - private final Map mimeTypeTexts = Maps.newHashMap(); - - /** - * Creates a new default output. - * - * @param summary - * The summary of the output - */ - public DefaultOutput(String summary) { - this.summary = summary; - } - - // - // ACTIONS - // - - /** - * Adds the given text for the given MIME type. - * - * @param mimeType - * The MIME type to add the text for - * @param text - * The text to add - * @return This default output - */ - public DefaultOutput addText(String mimeType, String text) { - mimeTypeTexts.put(mimeType, text); - return this; - } - - // - // OUTPUT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public String summary() { - return summary; - } - - /** - * {@inheritDoc} - */ - @Override - public String text(String mimeType, int maxLength) { - return mimeTypeTexts.get(mimeType); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/output/Output.java b/src/main/java/net/pterodactylus/reactor/output/Output.java deleted file mode 100644 index b3e26c7..0000000 --- a/src/main/java/net/pterodactylus/reactor/output/Output.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Reactor - Output.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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); - -} diff --git a/src/main/java/net/pterodactylus/reactor/package-info.java b/src/main/java/net/pterodactylus/reactor/package-info.java deleted file mode 100644 index 708319c..0000000 --- a/src/main/java/net/pterodactylus/reactor/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Reactor main definitions. - *

- * 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}. - *

- * 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. - *

- * 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. - *

- * If a system has been found to trigger, an {@code Action} is executed. It - * performs arbitrary actions and can use both the current state and the - * previous state to define that action. - */ - -package net.pterodactylus.reactor; - diff --git a/src/main/java/net/pterodactylus/reactor/queries/FileQuery.java b/src/main/java/net/pterodactylus/reactor/queries/FileQuery.java deleted file mode 100644 index ac0e895..0000000 --- a/src/main/java/net/pterodactylus/reactor/queries/FileQuery.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Reactor - FileQuery.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class FileQuery implements Query { - - /** The name of the file to query. */ - private final String filename; - - /** - * Creates a new file query. - * - * @param filename - * The name of the file to query - */ - public FileQuery(String filename) { - this.filename = checkNotNull(filename, "filename must not be null"); - } - - // - // QUERY METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public State state() { - File file = new File(filename); - if (!file.exists()) { - return new FileState(false, false, -1, -1); - } - if (!file.canRead()) { - return new FileState(true, false, -1, -1); - } - return new FileState(true, true, file.length(), file.lastModified()); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java b/src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java deleted file mode 100644 index 2cec118..0000000 --- a/src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Reactor - HttpQuery.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class HttpQuery implements Query { - - /** The uri to request. */ - private final String uri; - - /** - * Creates a new HTTP query. - * - * @param uri - * The uri to request - */ - public HttpQuery(String uri) { - this.uri = uri; - } - - // - // QUERY METHODS - // - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("deprecation") - public State state() { - DefaultHttpClient httpClient = new DefaultHttpClient(); - httpClient.addResponseInterceptor(new ResponseContentEncoding()); - HttpGet get = new HttpGet(uri); - - InputStreamReader inputStreamReader = null; - try { - /* make request. */ - get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11"); - HttpResponse response = httpClient.execute(get); - if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { - return new FailedState(); - } - HttpEntity entity = response.getEntity(); - - /* yay, done! */ - return new HttpState(uri, response.getStatusLine().getStatusCode(), entity.getContentType().getValue(), EntityUtils.toByteArray(entity)); - - } catch (IOException ioe1) { - return new FailedState(ioe1); - } finally { - Closeables.closeQuietly(inputStreamReader); - } - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/AbstractState.java b/src/main/java/net/pterodactylus/reactor/states/AbstractState.java deleted file mode 100644 index 6d7a6ea..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/AbstractState.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Reactor - AbstractState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") -public abstract class AbstractState implements State { - - /** The time of this state. */ - @JsonProperty - private final long time; - - /** Whether the state was successfully retrieved. */ - private final boolean success; - - /** The optional exception that occured while retrieving the state. */ - private final Throwable exception; - - /** The number of consecutive failures. */ - @JsonProperty - private int failCount; - - /** - * Creates a new successful state. - */ - protected AbstractState() { - this(true); - } - - /** - * Creates a new state. - * - * @param success - * {@code true} if the state is successful, {@code false} - * otherwise - */ - protected AbstractState(boolean success) { - this(success, null); - } - - /** - * Creates a new non-successful state with the given exception. - * - * @param exception - * The exception that occured while retrieving the state - */ - protected AbstractState(Throwable exception) { - this(false, exception); - } - - /** - * Creates a new state. - * - * @param success - * {@code true} if the state is successful, {@code false} - * otherwise - * @param exception - * The exception that occured while retrieving the state - */ - protected AbstractState(boolean success, Throwable exception) { - this.time = System.currentTimeMillis(); - this.success = success; - this.exception = exception; - } - - // - // STATE METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public long time() { - return time; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean success() { - return success; - } - - /** - * {@inheritDoc} - */ - @Override - public int failCount() { - return failCount; - } - - /** - * {@inheritDoc} - */ - @Override - public void setFailCount(int failCount) { - this.failCount = failCount; - } - - /** - * {@inheritDoc} - */ - @Override - public Throwable exception() { - return exception; - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/EpisodeState.java b/src/main/java/net/pterodactylus/reactor/states/EpisodeState.java deleted file mode 100644 index 1c120d6..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/EpisodeState.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Reactor - EpisodeState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class EpisodeState extends AbstractState implements Iterable { - - /** The episodes found in the current request. */ - @JsonProperty - private final List episodes = new ArrayList(); - - /** - * No-arg constructor for deserialization. - */ - @SuppressWarnings("unused") - private EpisodeState() { - this(Collections. emptySet()); - } - - /** - * Creates a new episode state. - * - * @param episodes - * The episodes of the request - */ - public EpisodeState(Collection episodes) { - this.episodes.addAll(episodes); - } - - // - // ACCESSORS - // - - /** - * Returns all episodes contained in this state. - * - * @return The episodes of this state - */ - public Collection episodes() { - return Collections.unmodifiableCollection(episodes); - } - - // - // ITERABLE INTERFACE - // - - /** - * {@inheritDoc} - */ - @Override - public Iterator iterator() { - return episodes.iterator(); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes); - } - - /** - * Stores attributes for an episode. - * - * @author David ‘Bombe’ Roden - */ - public static class Episode implements Iterable { - - /** 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 torrentFiles = new ArrayList(); - - /** - * 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 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 iterator() { - return torrentFiles.iterator(); - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return season * 65536 + episode; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Episode)) { - return false; - } - Episode episode = (Episode) obj; - return (season == episode.season) && (this.episode == episode.episode); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles); - } - - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/FailedState.java b/src/main/java/net/pterodactylus/reactor/states/FailedState.java deleted file mode 100644 index cfcbeef..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/FailedState.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Reactor - FailedState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor.states; - -import net.pterodactylus.reactor.State; - -/** - * {@link State} implementation that signals failure. - * - * @author David ‘Bombe’ Roden - */ -public class FailedState extends AbstractState { - - /** A failed state instance without an exception. */ - public static final State INSTANCE = new FailedState(); - - /** - * Creates a new failed state. - */ - public FailedState() { - super(false); - } - - /** - * Creates a new failed state with the given exception - * - * @param exception - * The exception of the state - */ - public FailedState(Throwable exception) { - super(exception); - } - - // - // STATIC METHODS - // - - /** - * Returns a failed state for the given state. The failed state will be - * unsuccessful ({@link #success()} returns false) and it will contain the - * same {@link #exception()} as the given state. - * - * @param state - * The state to copy the exception from - * @return A failed state - */ - public static FailedState from(State state) { - if (state instanceof FailedState) { - return (FailedState) state; - } - return new FailedState(state.exception()); - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[exception=%s]", getClass().getSimpleName(), exception()); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/FileState.java b/src/main/java/net/pterodactylus/reactor/states/FileState.java deleted file mode 100644 index b3bb70a..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/FileState.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Reactor - FileState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.reactor.states; - -import net.pterodactylus.reactor.State; - -/** - * A {@link State} that contains information about a file. - * - * @author David ‘Bombe’ Roden - */ -public class FileState extends AbstractState { - - /** Whether the file exists. */ - private final boolean exists; - - /** Whether the file is readable. */ - private final boolean readable; - - /** The size of the file. */ - private final long size; - - /** The modification time of the file. */ - private final long modificationTime; - - /** - * Creates a new file state that signals that an exceptio occured during - * retrieval. - * - * @param exception - * The exception that occured - */ - public FileState(Throwable exception) { - super(exception); - exists = false; - readable = false; - size = -1; - modificationTime = -1; - } - - /** - * Creates a new file state. - * - * @param exists - * {@code true} if the file exists, {@code false} otherwise - * @param readable - * {@code true} if the file is readable, {@code false} otherwise - * @param size - * The size of the file (in bytes) - * @param modificationTime - * The modification time of the file (in milliseconds since Jan - * 1, 1970 UTC) - */ - public FileState(boolean exists, boolean readable, long size, long modificationTime) { - this.exists = exists; - this.readable = readable; - this.size = size; - this.modificationTime = modificationTime; - } - - // - // ACCESSORS - // - - /** - * Returns whether the file exists. - * - * @return {@code true} if the file exists, {@code false} otherwise - */ - public boolean exists() { - return exists; - } - - /** - * Returns whether the file is readable. - * - * @return {@code true} if the file is readable, {@code false} otherwise - */ - public boolean readable() { - return readable; - } - - /** - * Returns the size of the file. - * - * @return The size of the file (in bytes) - */ - public long size() { - return size; - } - - /** - * Returns the modification time of the file. - * - * @return The modification time of the file (in milliseconds since Jan 1, - * 1970 UTC) - */ - public long modificationTime() { - return modificationTime; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[exists=%s,readable=%s,size=%s,modificationTime=%d(%5$tc)", getClass().getSimpleName(), exists(), readable(), size(), modificationTime()); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/HtmlState.java b/src/main/java/net/pterodactylus/reactor/states/HtmlState.java deleted file mode 100644 index ce89a98..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/HtmlState.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Reactor - HtmlState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class HtmlState extends AbstractState { - - /** The URI of the parsed document. */ - private final String uri; - - /** The parsed document. */ - private final Document document; - - /** - * Creates a new HTML state. - * - * @param uri - * The URI of the parsed document - * @param document - * The parsed documnet - */ - public HtmlState(String uri, Document document) { - this.uri = uri; - this.document = document; - } - - // - // ACCESSORS - // - - /** - * Returns the URI of the parsed document. - * - * @return The URI of the parsed document - */ - public String uri() { - return uri; - } - - /** - * Returns the parsed document. - * - * @return The parsed document - */ - public Document document() { - return document; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[document=(%s chars)]", getClass().getSimpleName(), document().toString().length()); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/HttpState.java b/src/main/java/net/pterodactylus/reactor/states/HttpState.java deleted file mode 100644 index 442106b..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/HttpState.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Reactor - HttpState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class HttpState extends AbstractState { - - /** The URI that was requested. */ - private final String uri; - - /** The protocol code. */ - private final int protocolCode; - - /** The content type. */ - private final String contentType; - - /** The result. */ - private final byte[] rawResult; - - /** - * Creates a new HTTP state. - * - * @param uri - * The URI that was requested - * @param protocolCode - * The code of the reply - * @param contentType - * The content type of the reply - * @param rawResult - * The raw result - */ - public HttpState(String uri, int protocolCode, String contentType, byte[] rawResult) { - this.uri = uri; - this.protocolCode = protocolCode; - this.contentType = contentType; - this.rawResult = rawResult; - } - - // - // ACCESSORS - // - - /** - * Returns the URI that was requested. - * - * @return The URI that was request - */ - public String uri() { - return uri; - } - - /** - * Returns the protocol code of the reply. - * - * @return The protocol code of the reply - */ - public int protocolCode() { - return protocolCode; - } - - /** - * Returns the content type of the reply. - * - * @return The content type of the reply - */ - public String contentType() { - return contentType; - } - - /** - * Returns the raw result of the reply. - * - * @return The raw result of the reply - */ - public byte[] rawResult() { - return rawResult; - } - - /** - * Returns the decoded content of the reply. This method uses the charset - * information from the {@link #contentType()}, if present, or UTF-8 if no - * content type is present. - * - * @return The decoded content - */ - public String content() { - try { - return new String(rawResult(), extractCharset(contentType())); - } catch (UnsupportedEncodingException uee1) { - throw new RuntimeException(String.format("Could not decode content as %s.", extractCharset(contentType())), uee1); - } - } - - // - // STATIC METHODS - // - - /** - * Extracts charset information from the given content type. - * - * @param contentType - * The content type response header - * @return The extracted charset, or UTF-8 if no charset could be extracted - */ - private static String extractCharset(String contentType) { - if (contentType == null) { - return "ISO-8859-1"; - } - HeaderElement headerElement = BasicHeaderValueParser.parseHeaderElement(contentType, new BasicHeaderValueParser()); - NameValuePair charset = headerElement.getParameterByName("charset"); - return (charset != null) ? charset.getValue() : "ISO-8859-1"; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s[uri=%s,protocolCode=%d,contentType=%s,rawResult=(%s bytes)]", getClass().getSimpleName(), uri(), protocolCode(), contentType(), rawResult().length); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/StateManager.java b/src/main/java/net/pterodactylus/reactor/states/StateManager.java deleted file mode 100644 index 8046153..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/StateManager.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Reactor - StateManager.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class StateManager { - - /** The logger. */ - private static final Logger logger = Logger.getLogger(StateManager.class); - - /** Jackson object mapper. */ - private final ObjectMapper objectMapper = new ObjectMapper(); - - /** The directory in which to store states. */ - private final String directory; - - /** - * Creates a new state manager. The given directory is assumed to exist. - * - * @param directory - * The directory to store states in - */ - public StateManager(String directory) { - this.directory = directory; - } - - // - // ACTIONS - // - - /** - * Loads the last state with the given name. - * - * @param reactionName - * The name of the reaction - * @return The loaded state, or {@code null} if the state could not be - * loaded - */ - public State loadLastState(String reactionName) { - return loadLastState(reactionName, false); - } - - /** - * Loads the last state with the given name. - * - * @param reactionName - * The name of the reaction - * @return The loaded state, or {@code null} if the state could not be - * loaded - */ - public State loadLastSuccessfulState(String reactionName) { - return loadLastState(reactionName, true); - } - - /** - * Saves the given state under the given name. - * - * @param reactionName - * The name of the reaction - * @param state - * The state to save - */ - public void saveState(String reactionName, State state) { - try { - File stateFile = stateFile(reactionName, "last"); - objectMapper.writeValue(stateFile, state); - if (state.success()) { - stateFile = stateFile(reactionName, "success"); - objectMapper.writeValue(stateFile, state); - } - } catch (JsonGenerationException jge1) { - logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jge1); - } catch (JsonMappingException jme1) { - logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jme1); - } catch (IOException ioe1) { - logger.warn(String.format("State for Reaction “%s” could not be written.", reactionName)); - } - } - - // - // PRIVATE METHODS - // - - /** - * Returns the file for the state with the given name. - * - * @param reactionName - * The name of the reaction - * @param suffix - * An additional suffix (may be {@code null} - * @return The file for the state - */ - private File stateFile(String reactionName, String suffix) { - return new File(directory, reactionName + ((suffix != null) ? "." + suffix : "") + ".json"); - } - - /** - * Load the given state for the reaction with the given name. - * - * @param reactionName - * The name of the reaction - * @param successful - * {@code true} to load the last successful state, {@code false} - * to load the last state - * @return The loaded state, or {@code null} if the state could not be - * loaded - */ - private State loadLastState(String reactionName, boolean successful) { - File stateFile = stateFile(reactionName, successful ? "success" : "last"); - try { - State state = objectMapper.readValue(stateFile, AbstractState.class); - return state; - } catch (JsonParseException jpe1) { - logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jpe1); - } catch (JsonMappingException jme1) { - logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jme1); - } catch (IOException ioe1) { - logger.info(String.format("State for Reaction “%s” could not be found.", reactionName)); - } - return null; - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/states/TorrentState.java b/src/main/java/net/pterodactylus/reactor/states/TorrentState.java deleted file mode 100644 index f3b3546..0000000 --- a/src/main/java/net/pterodactylus/reactor/states/TorrentState.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Reactor - TorrentState.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class TorrentState extends AbstractState implements Iterable { - - /** The torrent files. */ - @JsonProperty - private List 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 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 David ‘Bombe’ Roden - */ - 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 parameters = URLEncodedUtils.parse(magnetUri.substring("magnet:?".length()), Charset.forName("UTF-8")); - for (NameValuePair parameter : parameters) { - if (parameter.getName().equals("xt")) { - return parameter.getValue(); - } - } - return null; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return (generateId() != null) ? generateId().hashCode() : 0; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object object) { - if (!(object instanceof TorrentFile)) { - return false; - } - if (generateId() != null) { - return generateId().equals(((TorrentFile) object).generateId()); - } - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri()); - } - - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java deleted file mode 100644 index 5971553..0000000 --- a/src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Reactor - AlwaysTrigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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", "

true
"); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java deleted file mode 100644 index a285843..0000000 --- a/src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Reactor - FileExistenceTrigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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", "
File appeared/disappeared
"); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java deleted file mode 100644 index 39044c3..0000000 --- a/src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Reactor - FileStateModifiedTrigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -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", "
File modified
"); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java deleted file mode 100644 index 4504d60..0000000 --- a/src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Reactor - NewEpisodeTrigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class NewEpisodeTrigger implements Trigger { - - /** All new episodes. */ - private Collection newEpisodes; - - /** All changed episodes. */ - private Collection 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() { - - @Override - public boolean apply(Episode episode) { - return !previousEpisodeState.episodes().contains(episode); - } - }); - - changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate() { - - @Override - public boolean apply(Episode episode) { - if (!previousEpisodeState.episodes().contains(episode)) { - return false; - } - - /* find previous episode. */ - final Episode previousEpisode = findPreviousEpisode(episode); - - /* compare the list of torrent files. */ - Collection newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate() { - - @Override - public boolean apply(TorrentFile torrentFile) { - return !previousEpisode.torrentFiles().contains(torrentFile); - } - }); - - return !newTorrentFiles.isEmpty(); - } - - 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 newEpisodes, Collection 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 newEpisodes, Collection changedEpisodes) { - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append("\n"); - htmlBuilder.append("

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

\n"); - if (!newEpisodes.isEmpty()) { - htmlBuilder.append("

New Episodes

\n"); - htmlBuilder.append("
    \n"); - for (Episode episode : newEpisodes) { - htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); - htmlBuilder.append("
      \n"); - for (TorrentFile torrentFile : episode) { - htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); - htmlBuilder.append("
      "); - htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); - htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); - htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); - htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); - htmlBuilder.append("
      Magnet "); - htmlBuilder.append("Download
      \n"); - } - htmlBuilder.append("
    \n"); - } - htmlBuilder.append("
\n"); - } - if (!changedEpisodes.isEmpty()) { - htmlBuilder.append("

Changed Episodes

\n"); - htmlBuilder.append("
    \n"); - for (Episode episode : changedEpisodes) { - htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); - htmlBuilder.append("
      \n"); - for (TorrentFile torrentFile : episode) { - htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); - htmlBuilder.append("
      "); - htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); - htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); - htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); - htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); - htmlBuilder.append("
      Magnet "); - htmlBuilder.append("Download
      \n"); - } - htmlBuilder.append("
    \n"); - } - htmlBuilder.append("
\n"); - } - htmlBuilder.append("\n"); - return htmlBuilder.toString(); - } - -} diff --git a/src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java deleted file mode 100644 index ab640c6..0000000 --- a/src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Reactor - NewTorrentTrigger.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 David ‘Bombe’ Roden - */ -public class NewTorrentTrigger implements Trigger { - - /** The newly detected torrent files. */ - private List 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 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 torrentFiles) { - StringBuilder htmlText = new StringBuilder(); - htmlText.append("\n"); - htmlText.append("

New Torrents

\n"); - htmlText.append("
    \n"); - for (TorrentFile torrentFile : torrentFiles) { - htmlText.append("
  • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
  • "); - htmlText.append("
    Size: ").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(" in ").append(torrentFile.fileCount()).append(" file(s)
    "); - htmlText.append("
    ").append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)
    "); - htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri()))); - htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri()))); - } - htmlText.append("
\n"); - htmlText.append("\n"); - return htmlText.toString(); - } - -} diff --git a/src/main/java/net/pterodactylus/rhynodge/Action.java b/src/main/java/net/pterodactylus/rhynodge/Action.java new file mode 100644 index 0000000..4a7ed56 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/Action.java @@ -0,0 +1,38 @@ +/* + * Rhynodge - Action.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public interface Action { + + /** + * Performs the action. + * + * @param output + * The output for the action + */ + void execute(Output output); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/Filter.java b/src/main/java/net/pterodactylus/rhynodge/Filter.java new file mode 100644 index 0000000..b175b48 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/Filter.java @@ -0,0 +1,43 @@ +/* + * Rhynodge - Filter.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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. + *

+ * 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 David ‘Bombe’ Roden + */ +public interface Filter { + + /** + * Converts the given state into a different state. + * + * @param state + * The state to convert + * @return The new state + */ + State filter(State state); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/Query.java b/src/main/java/net/pterodactylus/rhynodge/Query.java new file mode 100644 index 0000000..82ce8b6 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/Query.java @@ -0,0 +1,35 @@ +/* + * Rhynodge - Query.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge; + +/** + * A query is used to retrieve the current {@link State} of a system. + * + * @author David ‘Bombe’ Roden + */ +public interface Query { + + /** + * Retrieves the current state of the system. The returned state is never + * {@code null}. + * + * @return The current state of the system. + */ + public State state(); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/Reaction.java b/src/main/java/net/pterodactylus/rhynodge/Reaction.java new file mode 100644 index 0000000..69e2592 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/Reaction.java @@ -0,0 +1,160 @@ +/* + * Rhynodge - Reaction.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 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. 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 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 filters() { + return filters; + } + + /** + * Returns the trigger to detect changes. + * + * @return The trigger to detect changes + */ + public Trigger trigger() { + return trigger; + } + + /** + * Returns the action to perform. + * + * @return The action to perform + */ + public Action action() { + return action; + } + + /** + * Returns the update interval of this reaction. + * + * @return The update interval of this reaction (in milliseconds) + */ + public long updateInterval() { + return updateInterval; + } + + /** + * Sets the update interval of this reaction. + * + * @param updateInterval + * The update interval of this reaction (in milliseconds) + * @return This reaction + */ + public Reaction setUpdateInterval(long updateInterval) { + this.updateInterval = updateInterval; + return this; + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/State.java b/src/main/java/net/pterodactylus/rhynodge/State.java new file mode 100644 index 0000000..fe7f8ea --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/State.java @@ -0,0 +1,77 @@ +/* + * Rhynodge - State.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge; + +/** + * Defines the current state of a system. + * + * @author David ‘Bombe’ Roden + */ +public interface State { + + /** + * Returns the time when this state was retrieved. + * + * @return The time when this state was retrieved (in millseconds since Jan + * 1, 1970 UTC) + */ + long time(); + + /** + * Whether the state was successfully retrieved. This method should only + * return {@code true} if a meaningful result could be retrieved; if e. g. a + * service is currently not reachable, this method should return false + * instead of emulating success by using empty lists or similar constructs. + * + * @return {@code true} if the state could be retrieved successfully, + * {@code false} otherwise + */ + boolean success(); + + /** + * Returns the number of consecutive failures. This method only returns a + * meaningful number iff {@link #success()} returns {@code false}. If + * {@link #success()} returns {@code false} for the first time after + * returning {@code true} and this method is called after {@link #success()} + * it will return {@code 1}. + * + * @return The number of consecutive failures + */ + int failCount(); + + /** + * Sets the fail count of this state. + * + * @param failCount + * The fail count of this state + */ + void setFailCount(int failCount); + + /** + * If {@link #success()} returns {@code false}, this method may return a + * {@link Throwable} to give some details for the reason why retrieving the + * state was not possible. For example, network-based {@link Query}s might + * return any exception that were encountered while communicating with the + * remote service. + * + * @return An exception that occured, may be {@code null} in case an + * exception can not be meaningfully returned + */ + Throwable exception(); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/Trigger.java b/src/main/java/net/pterodactylus/rhynodge/Trigger.java new file mode 100644 index 0000000..3b5a730 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/Trigger.java @@ -0,0 +1,55 @@ +/* + * Rhynodge - Trigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public interface Trigger { + + /** + * Checks whether the given states warrant a change trigger. + * + * @param currentState + * The current state of a system + * @param previousState + * The previous state of the system + * @return {@code true} if the given states warrant a change trigger, + * {@code false} otherwise + */ + boolean triggers(State currentState, State previousState); + + /** + * Returns the output of this trigger. This will only return a meaningful + * value if {@link #triggers(State, State)} returns {@code true}. + * + * @param reaction + * The reaction being triggered + * @return The output of this trigger + */ + Output output(Reaction reaction); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java b/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java new file mode 100644 index 0000000..6e866c8 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java @@ -0,0 +1,103 @@ +/* + * Rhynodge - EmailAction.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class EmailAction implements Action { + + /** The name of the SMTP host. */ + private final String hostname; + + /** The email address of the sender. */ + private final String sender; + + /** The email address of the recipient. */ + private final String recipient; + + /** + * Creates a new email action. + * + * @param hostname + * The hostname of the SMTP server + * @param sender + * The email address of the sender + * @param recipient + * The email address of the recipient + */ + public EmailAction(String hostname, String sender, String recipient) { + this.hostname = hostname; + this.sender = sender; + this.recipient = recipient; + } + + // + // ACTION METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public void execute(Output output) { + Properties properties = System.getProperties(); + properties.put("mail.smtp.host", hostname); + Session session = Session.getInstance(properties); + MimeMessage message = new MimeMessage(session); + try { + /* create message. */ + message.setFrom(new InternetAddress(sender)); + message.setRecipient(RecipientType.TO, new InternetAddress(recipient)); + message.setSubject(output.summary()); + + /* create text and html parts. */ + MimeMultipart multipart = new MimeMultipart(); + multipart.setSubType("alternative"); + MimeBodyPart textPart = new MimeBodyPart(); + textPart.setContent(output.text("text/plain", -1), "text/plain;charset=utf-8"); + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(output.text("text/html", -1), "text/html;charset=utf-8"); + multipart.addBodyPart(textPart); + multipart.addBodyPart(htmlPart); + message.setContent(multipart); + + Transport.send(message); + } catch (MessagingException me1) { + /* swallow. */ + } + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java b/src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java new file mode 100644 index 0000000..5791ecb --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java @@ -0,0 +1,39 @@ +/* + * Rhynodge - StandardOutAction.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class StandardOutAction implements Action { + + /** + * {@inheritDoc} + */ + @Override + public void execute(Output output) { + System.out.println(String.format("Triggered by %s.", output.text("text/plain", -1))); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java b/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java new file mode 100644 index 0000000..9e4d883 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java @@ -0,0 +1,211 @@ +/* + * Rhynodge - Engine.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 reactions = new HashMap(); + + /** + * 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> nextReactions = Maps.newTreeMap(); + String reactionName; + Reaction nextReaction; + synchronized (reactions) { + for (Entry reactionEntry : reactions.entrySet()) { + net.pterodactylus.rhynodge.State state = stateManager.loadLastState(reactionEntry.getKey()); + long stateTime = (state != null) ? state.time() : 0; + nextReactions.put(stateTime + reactionEntry.getValue().updateInterval(), Pair.of(reactionEntry.getKey(), reactionEntry.getValue())); + } + reactionName = nextReactions.get(nextReactions.firstKey()).getLeft(); + nextReaction = nextReactions.get(nextReactions.firstKey()).getRight(); + } + logger.debug(String.format("Next Reaction: %s.", reactionName)); + + /* wait until the next reaction has to run. */ + net.pterodactylus.rhynodge.State lastState = stateManager.loadLastState(reactionName); + long lastStateTime = (lastState != null) ? lastState.time() : 0; + int lastStateFailCount = (lastState != null) ? lastState.failCount() : 0; + long waitTime = (lastStateTime + nextReaction.updateInterval()) - System.currentTimeMillis(); + logger.debug(String.format("Time to wait for next Reaction: %d millseconds.", waitTime)); + if (waitTime > 0) { + synchronized (reactions) { + try { + logger.info(String.format("Waiting until %tc.", lastStateTime + nextReaction.updateInterval())); + reactions.wait(waitTime); + } catch (InterruptedException ie1) { + /* we’re looping! */ + } + } + + /* re-start loop to check for new reactions. */ + continue; + } + + /* run reaction. */ + logger.info(String.format("Running Query for %s...", reactionName)); + Query query = nextReaction.query(); + net.pterodactylus.rhynodge.State state; + try { + logger.debug("Querying system..."); + state = query.state(); + if (state == null) { + state = FailedState.INSTANCE; + } + logger.debug("System queried."); + } catch (Throwable t1) { + logger.warn("Querying system failed!", t1); + state = new AbstractState(t1) { + /* no further state. */ + }; + } + logger.debug(String.format("State is %s.", state)); + + /* convert states. */ + for (Filter filter : nextReaction.filters()) { + if (state.success()) { + net.pterodactylus.rhynodge.State newState = filter.filter(state); + logger.debug(String.format("Old state is %s, new state is %s.", state, newState)); + state = newState; + } + } + if (!state.success()) { + state.setFailCount(lastStateFailCount + 1); + } + net.pterodactylus.rhynodge.State lastSuccessfulState = stateManager.loadLastSuccessfulState(reactionName); + stateManager.saveState(reactionName, state); + + /* only run trigger if we have collected two successful states. */ + Trigger trigger = nextReaction.trigger(); + boolean triggerHit = false; + if ((lastSuccessfulState != null) && lastSuccessfulState.success() && state.success()) { + logger.debug("Checking Trigger for changes..."); + triggerHit = trigger.triggers(state, lastSuccessfulState); + } + + /* run action if trigger was hit. */ + logger.debug(String.format("Trigger was hit: %s.", triggerHit)); + if (triggerHit) { + logger.info("Executing Action..."); + nextReaction.action().execute(trigger.output(nextReaction)); + } + + } + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java b/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java new file mode 100644 index 0000000..a5d79ac --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java @@ -0,0 +1,83 @@ +/* + * Rhynodge - Starter.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 David ‘Bombe’ Roden + */ + private static interface Parameters { + + /** + * Returns the directory to watch for chains. + * + * @return The chain directory + */ + @Option(defaultValue = "chains", shortName = "c", description = "The directory to watch for chains") + String getChainDirectory(); + + /** + * Returns the directory to store states in. + * + * @return The states directory + */ + @Option(defaultValue = "states", shortName = "s", description = "The directory to store states in") + String getStateDirectory(); + + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java new file mode 100644 index 0000000..91fe098 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java @@ -0,0 +1,102 @@ +/* + * Rhynodge - EpisodeFilter.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 episodes = new LinkedHashMap(); + for (TorrentFile torrentFile : torrentState) { + Episode episode = extractEpisode(torrentFile); + if (episode == null) { + continue; + } + episodes.put(episode, episode); + episode = episodes.get(episode); + episode.addTorrentFile(torrentFile); + } + + return new EpisodeState(episodes.values()); + } + + // + // STATIC METHODS + // + + /** + * Extracts episode information from the given torrent file. + * + * @param torrentFile + * The torrent file to extract the episode information from + * @return The extracted episode information, or {@code null} if no episode + * information could be found + */ + private static Episode extractEpisode(TorrentFile torrentFile) { + Matcher matcher = episodePattern.matcher(torrentFile.name()); + if (!matcher.find()) { + return null; + } + String seasonString = matcher.group(1); + String episodeString = matcher.group(2); + if ((seasonString == null) && (episodeString == null)) { + seasonString = matcher.group(3); + episodeString = matcher.group(4); + } + int season = Integer.valueOf(seasonString); + int episode = Integer.valueOf(episodeString); + return new Episode(season, episode); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java new file mode 100644 index 0000000..d63f563 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java @@ -0,0 +1,51 @@ +/* + * Rhynodge - HtmlFilter.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class HtmlFilter implements Filter { + + /** + * {@inheritDoc} + */ + @Override + public State filter(State state) { + if (!state.success()) { + return FailedState.from(state); + } + checkState(state instanceof HttpState, "state is not a HttpState but a %s", state.getClass().getName()); + Document document = Jsoup.parse(((HttpState) state).content(), ((HttpState) state).uri()); + return new HtmlState(((HttpState) state).uri(), document); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java new file mode 100644 index 0000000..11865e7 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java @@ -0,0 +1,168 @@ +/* + * Rhynodge - KickAssTorrentsFilter.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class KickAssTorrentsFilter implements Filter { + + /** + * {@inheritDoc} + */ + @Override + public State filter(State state) { + if (!state.success()) { + return FailedState.from(state); + } + checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName()); + + /* get result table. */ + Document document = ((HtmlState) state).document(); + Elements mainTable = document.select("table.data"); + if (mainTable.isEmpty()) { + /* no main table? */ + return new FailedState(); + } + + /* iterate over all rows. */ + TorrentState torrentState = new TorrentState(); + Elements dataRows = mainTable.select("tr:gt(0)"); + for (Element dataRow : dataRows) { + String name = extractName(dataRow); + String size = extractSize(dataRow); + String magnetUri = extractMagnetUri(dataRow); + String downloadUri; + int fileCount = extractFileCount(dataRow); + int seedCount = extractSeedCount(dataRow); + int leechCount = extractLeechCount(dataRow); + try { + downloadUri = new URI(((HtmlState) state).uri()).resolve(extractDownloadUri(dataRow)).toString(); + TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount); + torrentState.addTorrentFile(torrentFile); + } catch (URISyntaxException use1) { + /* ignore; if uri was wrong, we wouldn’t be here. */ + } + } + + return torrentState; + } + + // + // STATIC METHODS + // + + /** + * Extracts the name from the given row. + * + * @param dataRow + * The row to extract the name from + * @return The extracted name + */ + private static String extractName(Element dataRow) { + return dataRow.select("div.torrentname a.normalgrey").text(); + } + + /** + * Extracts the size from the given row. + * + * @param dataRow + * The row to extract the size from + * @return The extracted size + */ + private static String extractSize(Element dataRow) { + return dataRow.select("td:eq(1)").text(); + } + + /** + * Extracts the magnet URI from the given row. + * + * @param dataRow + * The row to extract the magnet URI from + * @return The extracted magnet URI + */ + private static String extractMagnetUri(Element dataRow) { + return dataRow.select("a.imagnet").attr("href"); + } + + /** + * Extracts the download URI from the given row. + * + * @param dataRow + * The row to extract the download URI from + * @return The extracted download URI + */ + private static String extractDownloadUri(Element dataRow) { + return dataRow.select("a.idownload:not(.partner1Button)").attr("href"); + } + + /** + * Extracts the file count from the given row. + * + * @param dataRow + * The row to extract the file count from + * @return The extracted file count + */ + private static int extractFileCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(2)").text()); + } + + /** + * Extracts the seed count from the given row. + * + * @param dataRow + * The row to extract the seed count from + * @return The extracted seed count + */ + private static int extractSeedCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(4)").text()); + } + + /** + * Extracts the leech count from the given row. + * + * @param dataRow + * The row to extract the leech count from + * @return The extracted leech count + */ + private static int extractLeechCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(5)").text()); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/Chain.java b/src/main/java/net/pterodactylus/rhynodge/loader/Chain.java new file mode 100644 index 0000000..8fc6c35 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/loader/Chain.java @@ -0,0 +1,314 @@ +/* + * Rhynodge - Chain.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge.loader; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model for chain definitions. + * + * @author David ‘Bombe’ Roden + */ +public class Chain { + + /** + * Parameter model. + * + * @author David ‘Bombe’ Roden + */ + 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 David ‘Bombe’ Roden + */ + public static class Part { + + /** The class name of the part. */ + @JsonProperty(value = "class") + private String name; + + /** The parameters of the part. */ + @JsonProperty + private List parameters = new ArrayList(); + + /** + * 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 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 filters = new ArrayList(); + + /** 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 filters() { + return filters; + } + + /** + * Returns the trigger of this chain. + * + * @return The trigger of this chain + */ + public Part trigger() { + return trigger; + } + + /** + * Returns the action of this chain. + * + * @return The action of this chain + */ + public Part action() { + return action; + } + + /** + * Returns the update interval of the chain. + * + * @return The update interval (in seconds) + */ + public int updateInterval() { + return updateInterval; + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int hashCode = 0; + hashCode ^= name.hashCode(); + hashCode ^= query.hashCode(); + for (Part filter : filters) { + hashCode ^= filter.hashCode(); + } + hashCode ^= trigger.hashCode(); + hashCode ^= action.hashCode(); + hashCode ^= updateInterval; + return hashCode; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Chain)) { + return false; + } + Chain chain = (Chain) object; + if (!name.equals(chain.name)) { + return false; + } + if (!query.equals(chain.query)) { + return false; + } + if (filters.size() != chain.filters.size()) { + return false; + } + for (int filterIndex = 0; filterIndex < filters.size(); ++filterIndex) { + if (!filters.get(filterIndex).equals(chain.filters.get(filterIndex))) { + return false; + } + } + if (!trigger.equals(chain.trigger)) { + return false; + } + if (!action.equals(chain.action)) { + return false; + } + if (updateInterval != chain.updateInterval) { + return false; + } + return true; + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java b/src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java new file mode 100644 index 0000000..52b3fa4 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java @@ -0,0 +1,234 @@ +/* + * Rhynodge - ChainWatcher.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 loadedChains = new HashMap(); + + 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 chains = new HashMap(); + 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 enabledChains = Maps.filterEntries(chains, new Predicate>() { + + @Override + public boolean apply(Entry chainEntry) { + return chainEntry.getValue().enabled(); + } + }); + logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size())); + + /* check for removed chains. */ + Set chainsToRemove = new HashSet(); + for (Entry 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 enabledChain : enabledChains.entrySet()) { + + /* skip already loaded chains. */ + if (loadedChains.containsValue(enabledChain.getValue())) { + continue; + } + + logger.info(String.format("Loading new Chain: %s", enabledChain.getKey())); + + Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue()); + engine.addReaction(enabledChain.getKey(), reaction); + loadedChains.put(enabledChain.getKey(), enabledChain.getValue()); + } + + /* wait before checking again. */ + Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); + } + } + + // + // STATIC METHODS + // + + /** + * Parses the given configuration file into a {@link Chain}. + * + * @param configurationFile + * The configuration file to parse + * @return The parsed chain + */ + private static Chain parseConfigurationFile(File configurationFile) { + try { + return objectMapper.readValue(configurationFile, Chain.class); + } catch (JsonParseException jpe1) { + logger.warn(String.format("Could not parse %s.", configurationFile), jpe1); + } catch (JsonMappingException jme1) { + logger.warn(String.format("Could not parse %s.", configurationFile), jme1); + } catch (IOException ioe1) { + logger.info(String.format("Could not read %s.", configurationFile)); + } + return null; + } + + /** + * Extracts the name of the reaction from the given filename. + * + * @param filename + * The filename to extract the reaction name from + * @return The name of the reaction + */ + private static String getReactionName(String filename) { + return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename; + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java b/src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java new file mode 100644 index 0000000..badaadf --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java @@ -0,0 +1,66 @@ +/* + * Rhynodge - LoaderException.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge.loader; + +/** + * Exception that signals a problem when loading chain XML files. + * + * @author David ‘Bombe’ Roden + */ +public class LoaderException extends Exception { + + /** + * Creates a new loader exception. + */ + public LoaderException() { + super(); + } + + /** + * Creates a new loader exception. + * + * @param message + * The message of the exception + */ + public LoaderException(String message) { + super(message); + } + + /** + * Creates a new loader exception. + * + * @param throwable + * The root cause + */ + public LoaderException(Throwable throwable) { + super(throwable); + } + + /** + * Creates a new loader exception. + * + * @param message + * The message of the exception + * @param throwable + * The root cause + */ + public LoaderException(String message, Throwable throwable) { + super(message, throwable); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java b/src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java new file mode 100644 index 0000000..2cf1d21 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java @@ -0,0 +1,177 @@ +/* + * Rhynodge - Loader.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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 filters = new ArrayList(); + for (Part filterPart : chain.filters()) { + filters.add(ReactionLoader. 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 extractParameters(List parameters) { + List parameterValues = new ArrayList(); + + for (Parameter parameter : parameters) { + parameterValues.add(parameter.value()); + } + + return parameterValues; + } + + /** + * Creates a new object. + *

+ * 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. + *

+ * 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 createObject(String className, String packageName, List parameters) throws LoaderException { + + /* try to load class without package name. */ + Class objectClass = null; + try { + objectClass = Class.forName(className); + } catch (ClassNotFoundException cnfe1) { + /* ignore, we’ll try again. */ + } + + if (objectClass == null) { + try { + objectClass = Class.forName(packageName + "." + className); + } catch (ClassNotFoundException cnfe1) { + /* okay, now we need to throw. */ + throw new LoaderException(String.format("Could find neither class “%s” nor class “%s.”", className, packageName + "." + className), cnfe1); + } + } + + /* locate an eligible constructor. */ + Constructor wantedConstructor = null; + for (Constructor constructor : objectClass.getConstructors()) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != parameters.size()) { + continue; + } + boolean compatibleTypes = true; + for (Class parameterType : parameterTypes) { + if (parameterType != String.class) { + compatibleTypes = false; + break; + } + } + if (!compatibleTypes) { + continue; + } + wantedConstructor = constructor; + } + + if (wantedConstructor == null) { + throw new LoaderException("Could not find eligible constructor."); + } + + try { + return (T) wantedConstructor.newInstance(parameters.toArray()); + } catch (IllegalArgumentException iae1) { + throw new LoaderException("Could not invoke constructor.", iae1); + } catch (InstantiationException ie1) { + throw new LoaderException("Could not invoke constructor.", ie1); + } catch (IllegalAccessException iae1) { + throw new LoaderException("Could not invoke constructor.", iae1); + } catch (InvocationTargetException ite1) { + throw new LoaderException("Could not invoke constructor.", ite1); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java b/src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java new file mode 100644 index 0000000..bf77aff --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java @@ -0,0 +1,85 @@ +/* + * Rhynodge - DefaultOutput.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class DefaultOutput implements Output { + + /** The summary of the output. */ + private final String summary; + + /** The texts for the different MIME types. */ + private final Map mimeTypeTexts = Maps.newHashMap(); + + /** + * Creates a new default output. + * + * @param summary + * The summary of the output + */ + public DefaultOutput(String summary) { + this.summary = summary; + } + + // + // ACTIONS + // + + /** + * Adds the given text for the given MIME type. + * + * @param mimeType + * The MIME type to add the text for + * @param text + * The text to add + * @return This default output + */ + public DefaultOutput addText(String mimeType, String text) { + mimeTypeTexts.put(mimeType, text); + return this; + } + + // + // OUTPUT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String summary() { + return summary; + } + + /** + * {@inheritDoc} + */ + @Override + public String text(String mimeType, int maxLength) { + return mimeTypeTexts.get(mimeType); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/output/Output.java b/src/main/java/net/pterodactylus/rhynodge/output/Output.java new file mode 100644 index 0000000..dc058bb --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/output/Output.java @@ -0,0 +1,56 @@ +/* + * Rhynodge - Output.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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); + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/package-info.java b/src/main/java/net/pterodactylus/rhynodge/package-info.java new file mode 100644 index 0000000..2e7d9df --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/package-info.java @@ -0,0 +1,25 @@ +/** + * Rhynodge main definitions. + *

+ * 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}. + *

+ * 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. + *

+ * 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. + *

+ * If a system has been found to trigger, an {@code Action} is executed. It + * performs arbitrary actions and can use both the current state and the + * previous state to define that action. + */ + +package net.pterodactylus.rhynodge; + diff --git a/src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java b/src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java new file mode 100644 index 0000000..68f561e --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java @@ -0,0 +1,67 @@ +/* + * Rhynodge - FileQuery.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class FileQuery implements Query { + + /** The name of the file to query. */ + private final String filename; + + /** + * Creates a new file query. + * + * @param filename + * The name of the file to query + */ + public FileQuery(String filename) { + this.filename = checkNotNull(filename, "filename must not be null"); + } + + // + // QUERY METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public State state() { + File file = new File(filename); + if (!file.exists()) { + return new FileState(false, false, -1, -1); + } + if (!file.canRead()) { + return new FileState(true, false, -1, -1); + } + return new FileState(true, true, file.length(), file.lastModified()); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java b/src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java new file mode 100644 index 0000000..05f0830 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java @@ -0,0 +1,92 @@ +/* + * Rhynodge - HttpQuery.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class HttpQuery implements Query { + + /** The uri to request. */ + private final String uri; + + /** + * Creates a new HTTP query. + * + * @param uri + * The uri to request + */ + public HttpQuery(String uri) { + this.uri = uri; + } + + // + // QUERY METHODS + // + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public State state() { + DefaultHttpClient httpClient = new DefaultHttpClient(); + httpClient.addResponseInterceptor(new ResponseContentEncoding()); + HttpGet get = new HttpGet(uri); + + InputStreamReader inputStreamReader = null; + try { + /* make request. */ + get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11"); + HttpResponse response = httpClient.execute(get); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + return new FailedState(); + } + HttpEntity entity = response.getEntity(); + + /* yay, done! */ + return new HttpState(uri, response.getStatusLine().getStatusCode(), entity.getContentType().getValue(), EntityUtils.toByteArray(entity)); + + } catch (IOException ioe1) { + return new FailedState(ioe1); + } finally { + Closeables.closeQuietly(inputStreamReader); + } + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java b/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java new file mode 100644 index 0000000..17cd518 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java @@ -0,0 +1,135 @@ +/* + * Rhynodge - AbstractState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") +public abstract class AbstractState implements State { + + /** The time of this state. */ + @JsonProperty + private final long time; + + /** Whether the state was successfully retrieved. */ + private final boolean success; + + /** The optional exception that occured while retrieving the state. */ + private final Throwable exception; + + /** The number of consecutive failures. */ + @JsonProperty + private int failCount; + + /** + * Creates a new successful state. + */ + protected AbstractState() { + this(true); + } + + /** + * Creates a new state. + * + * @param success + * {@code true} if the state is successful, {@code false} + * otherwise + */ + protected AbstractState(boolean success) { + this(success, null); + } + + /** + * Creates a new non-successful state with the given exception. + * + * @param exception + * The exception that occured while retrieving the state + */ + protected AbstractState(Throwable exception) { + this(false, exception); + } + + /** + * Creates a new state. + * + * @param success + * {@code true} if the state is successful, {@code false} + * otherwise + * @param exception + * The exception that occured while retrieving the state + */ + protected AbstractState(boolean success, Throwable exception) { + this.time = System.currentTimeMillis(); + this.success = success; + this.exception = exception; + } + + // + // STATE METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public long time() { + return time; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean success() { + return success; + } + + /** + * {@inheritDoc} + */ + @Override + public int failCount() { + return failCount; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFailCount(int failCount) { + this.failCount = failCount; + } + + /** + * {@inheritDoc} + */ + @Override + public Throwable exception() { + return exception; + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java b/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java new file mode 100644 index 0000000..d7d90b2 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java @@ -0,0 +1,236 @@ +/* + * Rhynodge - EpisodeState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class EpisodeState extends AbstractState implements Iterable { + + /** The episodes found in the current request. */ + @JsonProperty + private final List episodes = new ArrayList(); + + /** + * No-arg constructor for deserialization. + */ + @SuppressWarnings("unused") + private EpisodeState() { + this(Collections. emptySet()); + } + + /** + * Creates a new episode state. + * + * @param episodes + * The episodes of the request + */ + public EpisodeState(Collection episodes) { + this.episodes.addAll(episodes); + } + + // + // ACCESSORS + // + + /** + * Returns all episodes contained in this state. + * + * @return The episodes of this state + */ + public Collection episodes() { + return Collections.unmodifiableCollection(episodes); + } + + // + // ITERABLE INTERFACE + // + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return episodes.iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes); + } + + /** + * Stores attributes for an episode. + * + * @author David ‘Bombe’ Roden + */ + public static class Episode implements Iterable { + + /** 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 torrentFiles = new ArrayList(); + + /** + * 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 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 iterator() { + return torrentFiles.iterator(); + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return season * 65536 + episode; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Episode)) { + return false; + } + Episode episode = (Episode) obj; + return (season == episode.season) && (this.episode == episode.episode); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java b/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java new file mode 100644 index 0000000..89aa8f2 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java @@ -0,0 +1,81 @@ +/* + * Rhynodge - FailedState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge.states; + +import net.pterodactylus.rhynodge.State; + +/** + * {@link State} implementation that signals failure. + * + * @author David ‘Bombe’ Roden + */ +public class FailedState extends AbstractState { + + /** A failed state instance without an exception. */ + public static final State INSTANCE = new FailedState(); + + /** + * Creates a new failed state. + */ + public FailedState() { + super(false); + } + + /** + * Creates a new failed state with the given exception + * + * @param exception + * The exception of the state + */ + public FailedState(Throwable exception) { + super(exception); + } + + // + // STATIC METHODS + // + + /** + * Returns a failed state for the given state. The failed state will be + * unsuccessful ({@link #success()} returns false) and it will contain the + * same {@link #exception()} as the given state. + * + * @param state + * The state to copy the exception from + * @return A failed state + */ + public static FailedState from(State state) { + if (state instanceof FailedState) { + return (FailedState) state; + } + return new FailedState(state.exception()); + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[exception=%s]", getClass().getSimpleName(), exception()); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FileState.java b/src/main/java/net/pterodactylus/rhynodge/states/FileState.java new file mode 100644 index 0000000..3c2e999 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/FileState.java @@ -0,0 +1,129 @@ +/* + * Rhynodge - FileState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.rhynodge.states; + +import net.pterodactylus.rhynodge.State; + +/** + * A {@link State} that contains information about a file. + * + * @author David ‘Bombe’ Roden + */ +public class FileState extends AbstractState { + + /** Whether the file exists. */ + private final boolean exists; + + /** Whether the file is readable. */ + private final boolean readable; + + /** The size of the file. */ + private final long size; + + /** The modification time of the file. */ + private final long modificationTime; + + /** + * Creates a new file state that signals that an exceptio occured during + * retrieval. + * + * @param exception + * The exception that occured + */ + public FileState(Throwable exception) { + super(exception); + exists = false; + readable = false; + size = -1; + modificationTime = -1; + } + + /** + * Creates a new file state. + * + * @param exists + * {@code true} if the file exists, {@code false} otherwise + * @param readable + * {@code true} if the file is readable, {@code false} otherwise + * @param size + * The size of the file (in bytes) + * @param modificationTime + * The modification time of the file (in milliseconds since Jan + * 1, 1970 UTC) + */ + public FileState(boolean exists, boolean readable, long size, long modificationTime) { + this.exists = exists; + this.readable = readable; + this.size = size; + this.modificationTime = modificationTime; + } + + // + // ACCESSORS + // + + /** + * Returns whether the file exists. + * + * @return {@code true} if the file exists, {@code false} otherwise + */ + public boolean exists() { + return exists; + } + + /** + * Returns whether the file is readable. + * + * @return {@code true} if the file is readable, {@code false} otherwise + */ + public boolean readable() { + return readable; + } + + /** + * Returns the size of the file. + * + * @return The size of the file (in bytes) + */ + public long size() { + return size; + } + + /** + * Returns the modification time of the file. + * + * @return The modification time of the file (in milliseconds since Jan 1, + * 1970 UTC) + */ + public long modificationTime() { + return modificationTime; + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[exists=%s,readable=%s,size=%s,modificationTime=%d(%5$tc)", getClass().getSimpleName(), exists(), readable(), size(), modificationTime()); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java b/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java new file mode 100644 index 0000000..64ac8a5 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java @@ -0,0 +1,84 @@ +/* + * Rhynodge - HtmlState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class HtmlState extends AbstractState { + + /** The URI of the parsed document. */ + private final String uri; + + /** The parsed document. */ + private final Document document; + + /** + * Creates a new HTML state. + * + * @param uri + * The URI of the parsed document + * @param document + * The parsed documnet + */ + public HtmlState(String uri, Document document) { + this.uri = uri; + this.document = document; + } + + // + // ACCESSORS + // + + /** + * Returns the URI of the parsed document. + * + * @return The URI of the parsed document + */ + public String uri() { + return uri; + } + + /** + * Returns the parsed document. + * + * @return The parsed document + */ + public Document document() { + return document; + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[document=(%s chars)]", getClass().getSimpleName(), document().toString().length()); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java b/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java new file mode 100644 index 0000000..95ea051 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java @@ -0,0 +1,154 @@ +/* + * Rhynodge - HttpState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class HttpState extends AbstractState { + + /** The URI that was requested. */ + private final String uri; + + /** The protocol code. */ + private final int protocolCode; + + /** The content type. */ + private final String contentType; + + /** The result. */ + private final byte[] rawResult; + + /** + * Creates a new HTTP state. + * + * @param uri + * The URI that was requested + * @param protocolCode + * The code of the reply + * @param contentType + * The content type of the reply + * @param rawResult + * The raw result + */ + public HttpState(String uri, int protocolCode, String contentType, byte[] rawResult) { + this.uri = uri; + this.protocolCode = protocolCode; + this.contentType = contentType; + this.rawResult = rawResult; + } + + // + // ACCESSORS + // + + /** + * Returns the URI that was requested. + * + * @return The URI that was request + */ + public String uri() { + return uri; + } + + /** + * Returns the protocol code of the reply. + * + * @return The protocol code of the reply + */ + public int protocolCode() { + return protocolCode; + } + + /** + * Returns the content type of the reply. + * + * @return The content type of the reply + */ + public String contentType() { + return contentType; + } + + /** + * Returns the raw result of the reply. + * + * @return The raw result of the reply + */ + public byte[] rawResult() { + return rawResult; + } + + /** + * Returns the decoded content of the reply. This method uses the charset + * information from the {@link #contentType()}, if present, or UTF-8 if no + * content type is present. + * + * @return The decoded content + */ + public String content() { + try { + return new String(rawResult(), extractCharset(contentType())); + } catch (UnsupportedEncodingException uee1) { + throw new RuntimeException(String.format("Could not decode content as %s.", extractCharset(contentType())), uee1); + } + } + + // + // STATIC METHODS + // + + /** + * Extracts charset information from the given content type. + * + * @param contentType + * The content type response header + * @return The extracted charset, or UTF-8 if no charset could be extracted + */ + private static String extractCharset(String contentType) { + if (contentType == null) { + return "ISO-8859-1"; + } + HeaderElement headerElement = BasicHeaderValueParser.parseHeaderElement(contentType, new BasicHeaderValueParser()); + NameValuePair charset = headerElement.getParameterByName("charset"); + return (charset != null) ? charset.getValue() : "ISO-8859-1"; + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s[uri=%s,protocolCode=%d,contentType=%s,rawResult=(%s bytes)]", getClass().getSimpleName(), uri(), protocolCode(), contentType(), rawResult().length); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/StateManager.java b/src/main/java/net/pterodactylus/rhynodge/states/StateManager.java new file mode 100644 index 0000000..19eccba --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/StateManager.java @@ -0,0 +1,154 @@ +/* + * Rhynodge - StateManager.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class StateManager { + + /** The logger. */ + private static final Logger logger = Logger.getLogger(StateManager.class); + + /** Jackson object mapper. */ + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** The directory in which to store states. */ + private final String directory; + + /** + * Creates a new state manager. The given directory is assumed to exist. + * + * @param directory + * The directory to store states in + */ + public StateManager(String directory) { + this.directory = directory; + } + + // + // ACTIONS + // + + /** + * Loads the last state with the given name. + * + * @param reactionName + * The name of the reaction + * @return The loaded state, or {@code null} if the state could not be + * loaded + */ + public State loadLastState(String reactionName) { + return loadLastState(reactionName, false); + } + + /** + * Loads the last state with the given name. + * + * @param reactionName + * The name of the reaction + * @return The loaded state, or {@code null} if the state could not be + * loaded + */ + public State loadLastSuccessfulState(String reactionName) { + return loadLastState(reactionName, true); + } + + /** + * Saves the given state under the given name. + * + * @param reactionName + * The name of the reaction + * @param state + * The state to save + */ + public void saveState(String reactionName, State state) { + try { + File stateFile = stateFile(reactionName, "last"); + objectMapper.writeValue(stateFile, state); + if (state.success()) { + stateFile = stateFile(reactionName, "success"); + objectMapper.writeValue(stateFile, state); + } + } catch (JsonGenerationException jge1) { + logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jge1); + } catch (JsonMappingException jme1) { + logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jme1); + } catch (IOException ioe1) { + logger.warn(String.format("State for Reaction “%s” could not be written.", reactionName)); + } + } + + // + // PRIVATE METHODS + // + + /** + * Returns the file for the state with the given name. + * + * @param reactionName + * The name of the reaction + * @param suffix + * An additional suffix (may be {@code null} + * @return The file for the state + */ + private File stateFile(String reactionName, String suffix) { + return new File(directory, reactionName + ((suffix != null) ? "." + suffix : "") + ".json"); + } + + /** + * Load the given state for the reaction with the given name. + * + * @param reactionName + * The name of the reaction + * @param successful + * {@code true} to load the last successful state, {@code false} + * to load the last state + * @return The loaded state, or {@code null} if the state could not be + * loaded + */ + private State loadLastState(String reactionName, boolean successful) { + File stateFile = stateFile(reactionName, successful ? "success" : "last"); + try { + State state = objectMapper.readValue(stateFile, AbstractState.class); + return state; + } catch (JsonParseException jpe1) { + logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jpe1); + } catch (JsonMappingException jme1) { + logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jme1); + } catch (IOException ioe1) { + logger.info(String.format("State for Reaction “%s” could not be found.", reactionName)); + } + return null; + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java new file mode 100644 index 0000000..ce5b06f --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java @@ -0,0 +1,305 @@ +/* + * Rhynodge - TorrentState.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class TorrentState extends AbstractState implements Iterable { + + /** The torrent files. */ + @JsonProperty + private List 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 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 David ‘Bombe’ Roden + */ + 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 parameters = URLEncodedUtils.parse(magnetUri.substring("magnet:?".length()), Charset.forName("UTF-8")); + for (NameValuePair parameter : parameters) { + if (parameter.getName().equals("xt")) { + return parameter.getValue(); + } + } + return null; + } + + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (generateId() != null) ? generateId().hashCode() : 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof TorrentFile)) { + return false; + } + if (generateId() != null) { + return generateId().equals(((TorrentFile) object).generateId()); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri()); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java new file mode 100644 index 0000000..3716894 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java @@ -0,0 +1,49 @@ +/* + * Rhynodge - AlwaysTrigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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", "

true
"); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java new file mode 100644 index 0000000..49d91b2 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java @@ -0,0 +1,58 @@ +/* + * Rhynodge - FileExistenceTrigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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", "
File appeared/disappeared
"); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java new file mode 100644 index 0000000..ed24d9e --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java @@ -0,0 +1,57 @@ +/* + * Rhynodge - FileStateModifiedTrigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +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", "
File modified
"); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java new file mode 100644 index 0000000..f23869d --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java @@ -0,0 +1,233 @@ +/* + * Rhynodge - NewEpisodeTrigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class NewEpisodeTrigger implements Trigger { + + /** All new episodes. */ + private Collection newEpisodes; + + /** All changed episodes. */ + private Collection 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() { + + @Override + public boolean apply(Episode episode) { + return !previousEpisodeState.episodes().contains(episode); + } + }); + + changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate() { + + @Override + public boolean apply(Episode episode) { + if (!previousEpisodeState.episodes().contains(episode)) { + return false; + } + + /* find previous episode. */ + final Episode previousEpisode = findPreviousEpisode(episode); + + /* compare the list of torrent files. */ + Collection newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate() { + + @Override + public boolean apply(TorrentFile torrentFile) { + return !previousEpisode.torrentFiles().contains(torrentFile); + } + }); + + return !newTorrentFiles.isEmpty(); + } + + 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 newEpisodes, Collection 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 newEpisodes, Collection changedEpisodes) { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append("\n"); + htmlBuilder.append("

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

\n"); + if (!newEpisodes.isEmpty()) { + htmlBuilder.append("

New Episodes

\n"); + htmlBuilder.append("
    \n"); + for (Episode episode : newEpisodes) { + htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); + htmlBuilder.append("
      \n"); + for (TorrentFile torrentFile : episode) { + htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); + htmlBuilder.append("
      "); + htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); + htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); + htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); + htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); + htmlBuilder.append("
      Magnet "); + htmlBuilder.append("Download
      \n"); + } + htmlBuilder.append("
    \n"); + } + htmlBuilder.append("
\n"); + } + if (!changedEpisodes.isEmpty()) { + htmlBuilder.append("

Changed Episodes

\n"); + htmlBuilder.append("
    \n"); + for (Episode episode : changedEpisodes) { + htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); + htmlBuilder.append("
      \n"); + for (TorrentFile torrentFile : episode) { + htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); + htmlBuilder.append("
      "); + htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); + htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); + htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); + htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); + htmlBuilder.append("
      Magnet "); + htmlBuilder.append("Download
      \n"); + } + htmlBuilder.append("
    \n"); + } + htmlBuilder.append("
\n"); + } + htmlBuilder.append("\n"); + return htmlBuilder.toString(); + } + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java new file mode 100644 index 0000000..a21a8d6 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java @@ -0,0 +1,130 @@ +/* + * Rhynodge - NewTorrentTrigger.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 David ‘Bombe’ Roden + */ +public class NewTorrentTrigger implements Trigger { + + /** The newly detected torrent files. */ + private List 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 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 torrentFiles) { + StringBuilder htmlText = new StringBuilder(); + htmlText.append("\n"); + htmlText.append("

New Torrents

\n"); + htmlText.append("
    \n"); + for (TorrentFile torrentFile : torrentFiles) { + htmlText.append("
  • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
  • "); + htmlText.append("
    Size: ").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(" in ").append(torrentFile.fileCount()).append(" file(s)
    "); + htmlText.append("
    ").append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)
    "); + htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri()))); + htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri()))); + } + htmlText.append("
\n"); + htmlText.append("\n"); + return htmlText.toString(); + } + +} diff --git a/src/main/resources/chains/kickasstorrents-example.json b/src/main/resources/chains/kickasstorrents-example.json index dbf0c7a..2962ea2 100644 --- a/src/main/resources/chains/kickasstorrents-example.json +++ b/src/main/resources/chains/kickasstorrents-example.json @@ -40,11 +40,11 @@ }, { "name": "sender", - "value": "reactor@reactor.de" + "value": "rhynodge@rhynodge.net" }, { "name": "recipient", - "value": "recipient@recipient.de" + "value": "recipient@recipient.net" } ] }, diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties index bcdf5a8..77db92e 100644 --- a/src/main/resources/log4j.properties +++ b/src/main/resources/log4j.properties @@ -4,5 +4,5 @@ log4j.appender.CA.layout=org.apache.log4j.PatternLayout log4j.appender.CA.layout.ConversionPattern=%d [%t] %p %c %m%n # some detailed logger settings -log4j.logger.net.pterodactylus.reactor.loader.ChainWatcher=INFO +log4j.logger.net.pterodactylus.rhynodge.loader.ChainWatcher=INFO log4j.logger.org.apache.http.wire=INFO