Send an email if a reaction fails with an exception
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 4 May 2015 17:10:23 +0000 (19:10 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 4 May 2015 17:10:23 +0000 (19:10 +0200)
src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/engine/Engine.java
src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java
src/main/java/net/pterodactylus/rhynodge/engine/Starter.java
src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json [new file with mode: 0644]

diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java b/src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java
new file mode 100644 (file)
index 0000000..9f1ca84
--- /dev/null
@@ -0,0 +1,41 @@
+package net.pterodactylus.rhynodge.engine;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Stores general configuration of Rhynodge.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Configuration {
+
+       @JsonProperty
+       private final String smtpHostname = null;
+
+       @JsonProperty
+       private String errorEmailSender = null;
+
+       @JsonProperty
+       private String errorEmailRecipient = null;
+
+       public String getSmtpHostname() {
+               return smtpHostname;
+       }
+
+       public String getErrorEmailSender() {
+               return errorEmailSender;
+       }
+
+       public String getErrorEmailRecipient() {
+               return errorEmailRecipient;
+       }
+
+       public static Configuration from(InputStream inputStream) throws IOException {
+               return new ObjectMapper().readValue(inputStream, Configuration.class);
+       }
+
+}
index 5c289c5..7103dc5 100644 (file)
@@ -29,6 +29,7 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 
 import net.pterodactylus.rhynodge.Reaction;
+import net.pterodactylus.rhynodge.actions.EmailAction;
 import net.pterodactylus.rhynodge.states.StateManager;
 
 /**
@@ -41,15 +42,11 @@ public class Engine {
        private final StateManager stateManager;
        private final ScheduledExecutorService executorService;
        private final Map<String, Future<?>> scheduledFutures = new ConcurrentHashMap<>();
+       private final EmailAction errorEmailAction;
 
-       /**
-        * Creates a new engine.
-        *
-        * @param stateManager
-        *              The state manager
-        */
-       public Engine(StateManager stateManager) {
+       public Engine(StateManager stateManager, EmailAction errorEmailAction) {
                this.stateManager = stateManager;
+               this.errorEmailAction = errorEmailAction;
                executorService = new ScheduledThreadPoolExecutor(10);
        }
 
@@ -70,7 +67,7 @@ public class Engine {
                Optional<net.pterodactylus.rhynodge.State> lastState = reactionState.loadLastState();
                long lastExecutionTime = lastState.map(net.pterodactylus.rhynodge.State::time).orElse(0L);
                long nextExecutionTime = lastExecutionTime + reaction.updateInterval();
-               ReactionRunner reactionRunner = new ReactionRunner(reaction, reactionState);
+               ReactionRunner reactionRunner = new ReactionRunner(reaction, reactionState, errorEmailAction);
                ScheduledFuture<?> future = executorService.scheduleWithFixedDelay(reactionRunner, nextExecutionTime - currentTimeMillis(), reaction.updateInterval(), MILLISECONDS);
                scheduledFutures.put(name, future);
        }
index ac25e18..c9af294 100644 (file)
@@ -5,6 +5,9 @@ import static java.util.Optional.ofNullable;
 import static net.pterodactylus.rhynodge.states.FailedState.INSTANCE;
 import static org.apache.log4j.Logger.getLogger;
 
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.Optional;
 
 import net.pterodactylus.rhynodge.Action;
@@ -13,6 +16,9 @@ import net.pterodactylus.rhynodge.Query;
 import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.Trigger;
+import net.pterodactylus.rhynodge.actions.EmailAction;
+import net.pterodactylus.rhynodge.output.DefaultOutput;
+import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.FailedState;
 
 import org.apache.log4j.Logger;
@@ -29,10 +35,12 @@ public class ReactionRunner implements Runnable {
        private static final Logger logger = getLogger(ReactionRunner.class);
        private final Reaction reaction;
        private final ReactionState reactionState;
+       private final EmailAction errorEmailAction;
 
-       public ReactionRunner(Reaction reaction, ReactionState reactionState) {
+       public ReactionRunner(Reaction reaction, ReactionState reactionState, EmailAction errorEmailAction) {
                this.reactionState = reactionState;
                this.reaction = reaction;
+               this.errorEmailAction = errorEmailAction;
        }
 
        @Override
@@ -42,6 +50,7 @@ public class ReactionRunner implements Runnable {
                if (!state.success()) {
                        logger.info(format("Reaction %s failed.", reaction.name()));
                        saveStateWithIncreasedFailCount(state);
+                       errorEmailAction.execute(createErrorOutput(reaction, state));
                        return;
                }
                Optional<State> lastSuccessfulState = reactionState.loadLastSuccessfulState();
@@ -66,6 +75,32 @@ public class ReactionRunner implements Runnable {
                reactionState.saveState(state);
        }
 
+       private Output createErrorOutput(Reaction reaction, State state) {
+               DefaultOutput output = new DefaultOutput(String.format("Error while processing “%s!”", reaction.name()));
+               output.addText("text/plain; charset=utf-8", createEmailText(reaction, state));
+               return output;
+       }
+
+       private String createEmailText(Reaction reaction, State state) {
+               StringBuilder emailText = new StringBuilder();
+               emailText.append(String.format("An error occured while processing “.”\n\n", reaction.name()));
+               appendExceptionToEmailText(state.exception(), emailText);
+               return emailText.toString();
+       }
+
+       private void appendExceptionToEmailText(Throwable exception, StringBuilder emailText) {
+               if (exception != null) {
+                       try (StringWriter stringWriter = new StringWriter();
+                                PrintWriter printWriter = new PrintWriter(stringWriter)) {
+                               exception.printStackTrace(printWriter);
+                               emailText.append(stringWriter.toString());
+                       } catch (IOException ioe1) {
+                               /* StringWriter doesn’t throw. */
+                               throw new RuntimeException(ioe1);
+                       }
+               }
+       }
+
        private State runQuery() {
                logger.info(format("Querying %s...", reaction.name()));
                try {
index 4c3b4db..05aff64 100644 (file)
 
 package net.pterodactylus.rhynodge.engine;
 
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import net.pterodactylus.rhynodge.actions.EmailAction;
 import net.pterodactylus.rhynodge.loader.ChainWatcher;
 import net.pterodactylus.rhynodge.states.StateManager;
 
@@ -36,22 +40,33 @@ public class Starter {
         * @param arguments
         *            Command-line arguments
         */
-       public static void main(String... arguments) {
+       public static void main(String... arguments) throws IOException {
 
                /* parse command line. */
                Parameters parameters = CliFactory.parseArguments(Parameters.class, arguments);
+               Configuration configuration = loadConfiguration(parameters.getConfigurationFile());
 
                /* create the state manager. */
                StateManager stateManager = new StateManager(parameters.getStateDirectory());
 
                /* create the engine. */
-               Engine engine = new Engine(stateManager);
+               Engine engine = new Engine(stateManager, createErrorEmailAction(configuration));
 
                /* start a watcher. */
                ChainWatcher chainWatcher = new ChainWatcher(engine, parameters.getChainDirectory());
                chainWatcher.start();
        }
 
+       private static Configuration loadConfiguration(String configurationFile) throws IOException {
+               try (FileInputStream configInputStream = new FileInputStream(configurationFile)) {
+                       return Configuration.from(configInputStream);
+               }
+       }
+
+       private static EmailAction createErrorEmailAction(Configuration configuration) {
+               return new EmailAction(configuration.getSmtpHostname(), configuration.getErrorEmailSender(), configuration.getErrorEmailRecipient());
+       }
+
        /**
         * Definition of the command-line parameters.
         *
@@ -75,6 +90,9 @@ public class Starter {
                @Option(defaultValue = "states", longName = "states", shortName = "s", description = "The directory to store states in")
                String getStateDirectory();
 
+               @Option(defaultValue = "/etc/rhynodge/rhynodge.json", longName = "config", shortName = "C", description = "The name of the configuration file")
+               String getConfigurationFile();
+
        }
 
 }
diff --git a/src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java b/src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java
new file mode 100644 (file)
index 0000000..6a497e0
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.rhynodge.engine;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Configuration}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationTest {
+
+       @Test
+       public void configurationCanBeReadFromJsonFile() throws IOException {
+               InputStream inputStream = getClass().getResourceAsStream("configuration.json");
+               Configuration configuration = Configuration.from(inputStream);
+               MatcherAssert.assertThat(configuration.getSmtpHostname(), Matchers.is("localhost"));
+               MatcherAssert.assertThat(configuration.getErrorEmailSender(), Matchers.is("errors@rhynodge.net"));
+               MatcherAssert.assertThat(configuration.getErrorEmailRecipient(), Matchers.is("errors@user.net"));
+       }
+
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json b/src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json
new file mode 100644 (file)
index 0000000..799a773
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "smtpHostname": "localhost",
+  "errorEmailSender": "errors@rhynodge.net",
+  "errorEmailRecipient": "errors@user.net"
+}