✨ Only send an email if previous state was not a failure, too
[rhynodge.git] / src / main / java / net / pterodactylus / rhynodge / engine / ReactionRunner.java
1 package net.pterodactylus.rhynodge.engine;
2
3 import static java.lang.String.format;
4 import static java.util.Optional.ofNullable;
5 import static net.pterodactylus.rhynodge.states.FailedState.INSTANCE;
6 import static org.apache.log4j.Logger.getLogger;
7
8 import java.io.IOException;
9 import java.io.PrintWriter;
10 import java.io.StringWriter;
11 import java.util.Optional;
12
13 import net.pterodactylus.rhynodge.Action;
14 import net.pterodactylus.rhynodge.Filter;
15 import net.pterodactylus.rhynodge.Query;
16 import net.pterodactylus.rhynodge.Reaction;
17 import net.pterodactylus.rhynodge.State;
18 import net.pterodactylus.rhynodge.actions.EmailAction;
19 import net.pterodactylus.rhynodge.Merger;
20 import net.pterodactylus.rhynodge.output.DefaultOutput;
21 import net.pterodactylus.rhynodge.output.Output;
22 import net.pterodactylus.rhynodge.states.FailedState;
23
24 import org.apache.log4j.Logger;
25
26 /**
27  * Runs a {@link Reaction}, starting with its {@link Query}, running the {@link
28  * State} through its {@link Filter}s, and finally checking the {@link Merger}
29  * for whether an {@link Action} needs to be executed.
30  *
31  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
32  */
33 public class ReactionRunner implements Runnable {
34
35         private static final Logger logger = getLogger(ReactionRunner.class);
36         private final Reaction reaction;
37         private final ReactionState reactionState;
38         private final EmailAction errorEmailAction;
39
40         public ReactionRunner(Reaction reaction, ReactionState reactionState, EmailAction errorEmailAction) {
41                 this.reactionState = reactionState;
42                 this.reaction = reaction;
43                 this.errorEmailAction = errorEmailAction;
44         }
45
46         @Override
47         public void run() {
48                 State state = runQuery();
49                 state = runStateThroughFilters(state);
50                 if (!state.success()) {
51                         logger.info(format("Reaction %s failed in %s.", reaction.name(), state));
52                         Optional<State> lastState = reactionState.loadLastState();
53                         saveStateWithIncreasedFailCount(state);
54                         if (thisFailureIsTheFirstFailure(lastState)) {
55                                 errorEmailAction.execute(createErrorOutput(reaction, state));
56                         }
57                         return;
58                 }
59                 Optional<State> lastSuccessfulState = reactionState.loadLastSuccessfulState();
60                 if (!lastSuccessfulState.isPresent()) {
61                         logger.info(format("No last state for %s.", reaction.name()));
62                         reactionState.saveState(state);
63                         return;
64                 }
65                 Merger merger = reaction.merger();
66                 State newState = merger.mergeStates(lastSuccessfulState.get(), state);
67                 reactionState.saveState(newState);
68                 if (newState.triggered()) {
69                         logger.info(format("Trigger was hit for %s, executing action...", reaction.name()));
70                         reaction.action().execute(newState.output(reaction));
71                 }
72                 logger.info(format("Reaction %s finished.", reaction.name()));
73         }
74
75         private static boolean thisFailureIsTheFirstFailure(Optional<State> lastState) {
76                 return lastState.map(State::success).orElse(true);
77         }
78
79         private void saveStateWithIncreasedFailCount(State state) {
80                 Optional<State> lastState = reactionState.loadLastState();
81                 state.setFailCount(lastState.map(State::failCount).orElse(0) + 1);
82                 reactionState.saveState(state);
83         }
84
85         private Output createErrorOutput(Reaction reaction, State state) {
86                 DefaultOutput output = new DefaultOutput(String.format("Error while processing “%s!”", reaction.name()));
87                 output.addText("text/plain", createErrorEmailText(reaction, state));
88                 output.addText("text/html", createErrorEmailText(reaction, state));
89                 return output;
90         }
91
92         private String createErrorEmailText(Reaction reaction, State state) {
93                 StringBuilder emailText = new StringBuilder();
94                 emailText.append(String.format("An error occured while processing “%s.”\n\n", reaction.name()));
95                 appendExceptionToEmailText(state.exception(), emailText);
96                 return emailText.toString();
97         }
98
99         private void appendExceptionToEmailText(Throwable exception, StringBuilder emailText) {
100                 if (exception != null) {
101                         try (StringWriter stringWriter = new StringWriter();
102                                  PrintWriter printWriter = new PrintWriter(stringWriter)) {
103                                 exception.printStackTrace(printWriter);
104                                 emailText.append(stringWriter.toString());
105                         } catch (IOException ioe1) {
106                                 /* StringWriter doesn’t throw. */
107                                 throw new RuntimeException(ioe1);
108                         }
109                 }
110         }
111
112         private State runQuery() {
113                 logger.info(format("Querying %s...", reaction.name()));
114                 try {
115                         return ofNullable(reaction.query().state()).orElse(INSTANCE);
116                 } catch (Throwable t1) {
117                         logger.warn(format("Could not query %s.", reaction.name()), t1);
118                         return new FailedState(t1);
119                 }
120         }
121
122         private State runStateThroughFilters(State state) {
123                 State currentState = state;
124                 for (Filter filter : reaction.filters()) {
125                         if (currentState.success()) {
126                                 logger.debug(format("Filtering state through %s...", filter.getClass().getSimpleName()));
127                                 try {
128                                         currentState = filter.filter(currentState);
129                                 } catch (Throwable t1) {
130                                         logger.warn(format("Error during filter %s for %s.", filter.getClass().getSimpleName(), reaction.name()), t1);
131                                         return new FailedState(t1);
132                                 }
133                         }
134                 }
135                 return currentState;
136         }
137
138 }