🐛 Set output of last-state merger to triggered
[rhynodge.git] / src / main / java / net / pterodactylus / rhynodge / loader / ChainWatcher.java
1 /*
2  * Rhynodge - ChainWatcher.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.rhynodge.loader;
19
20 import java.io.File;
21 import java.io.FilenameFilter;
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.Set;
28 import java.util.concurrent.TimeUnit;
29
30 import javax.inject.Inject;
31 import javax.inject.Singleton;
32
33 import net.pterodactylus.rhynodge.Reaction;
34 import net.pterodactylus.rhynodge.engine.Engine;
35 import net.pterodactylus.rhynodge.loader.Chain.Parameter;
36 import net.pterodactylus.rhynodge.loader.Chain.Part;
37
38 import org.apache.log4j.Logger;
39
40 import com.fasterxml.jackson.core.JsonParseException;
41 import com.fasterxml.jackson.databind.JsonMappingException;
42 import com.fasterxml.jackson.databind.ObjectMapper;
43 import com.google.common.base.Predicate;
44 import com.google.common.collect.Maps;
45 import com.google.common.util.concurrent.AbstractExecutionThreadService;
46 import com.google.common.util.concurrent.Uninterruptibles;
47
48 /**
49  * Watches a directory for chain configuration files and loads and unloads
50  * {@link Reaction}s from the {@link Engine}.
51  *
52  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
53  */
54 @Singleton
55 public class ChainWatcher extends AbstractExecutionThreadService {
56
57         /** The logger. */
58         private static final Logger logger = Logger.getLogger(ChainWatcher.class);
59
60         /** The JSON object mapper. */
61         private static final ObjectMapper objectMapper = new ObjectMapper();
62
63         /** The reaction loader. */
64         private final ReactionLoader reactionLoader = new ReactionLoader();
65
66         /** The engine to load reactions with. */
67         private final Engine engine;
68
69         /** The directory to watch for chain configuration files. */
70         private final String directory;
71
72         /**
73          * Creates a new chain watcher.
74          *
75          * @param engine
76          *            The engine to load reactions with
77          * @param directory
78          *            The directory to watch
79          */
80         @Inject
81         public ChainWatcher(Engine engine, ChainDirectory directory) {
82                 this.engine = engine;
83                 this.directory = directory.getDirectory();
84         }
85
86         //
87         // ABSTRACTEXECUTIONTHREADSERVICE METHODS
88         //
89
90         /**
91          * {@inheritDoc}
92          */
93         @Override
94         protected void run() throws Exception {
95
96                 /* loaded chains. */
97                 final Map<String, Chain> loadedChains = new HashMap<String, Chain>();
98
99                 while (isRunning()) {
100
101                         /* check if directory is there. */
102                         File directoryFile = new File(directory);
103                         if (!directoryFile.exists() || !directoryFile.isDirectory() || !directoryFile.canRead()) {
104                                 Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
105                                 continue;
106                         }
107
108                         /* list all files, scan for configuration files. */
109                         logger.debug(String.format("Scanning %s...", directory));
110                         File[] configurationFiles = directoryFile.listFiles(new FilenameFilter() {
111
112                                 @Override
113                                 public boolean accept(File dir, String name) {
114                                         return name.endsWith(".json");
115                                 }
116                         });
117                         logger.debug(String.format("Found %d configuration file(s), parsing...", configurationFiles.length));
118
119                         /* now parse all XML files. */
120                         Map<String, Chain> chains = new HashMap<String, Chain>();
121                         for (File configurationFile : configurationFiles) {
122
123                                 /* parse XML file. */
124                                 Chain chain = parseConfigurationFile(configurationFile);
125                                 if (chain == null) {
126                                         logger.warn(String.format("Could not parse %s.", configurationFile));
127                                         continue;
128                                 }
129
130                                 /* dump chain */
131                                 logger.debug(String.format(" Enabled: %s", chain.enabled()));
132
133                                 if (chain.watcher() != null) {
134                                         logger.debug(String.format("Reaction: %s", chain.watcher().name()));
135                                 } else {
136                                         logger.debug(String.format(" Query: %s", chain.query().name()));
137                                         for (Parameter parameter : chain.query().parameters()) {
138                                                 logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
139                                         }
140                                         for (Part filter : chain.filters()) {
141                                                 logger.debug(String.format(" Filter: %s", filter.name()));
142                                                 for (Parameter parameter : filter.parameters()) {
143                                                         logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
144                                                 }
145                                         }
146                                         logger.debug(String.format(" Trigger: %s", chain.merger().name()));
147                                         for (Parameter parameter : chain.merger().parameters()) {
148                                                 logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
149                                         }
150                                 }
151                                 logger.debug(String.format(" Action: %s", chain.action().name()));
152                                 for (Parameter parameter : chain.action().parameters()) {
153                                         logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
154                                 }
155
156                                 chains.put(getReactionName(configurationFile.getName()), chain);
157                         }
158
159                         /* filter enabled chains. */
160                         Map<String, Chain> enabledChains = Maps.filterEntries(chains, new Predicate<Entry<String, Chain>>() {
161
162                                 @Override
163                                 public boolean apply(Entry<String, Chain> chainEntry) {
164                                         return chainEntry.getValue().enabled();
165                                 }
166                         });
167                         logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size()));
168
169                         /* check for removed chains. */
170                         Set<String> chainsToRemove = new HashSet<String>();
171                         for (Entry<String, Chain> loadedChain : loadedChains.entrySet()) {
172
173                                 /* skip chains that still exist. */
174                                 if (enabledChains.containsKey(loadedChain.getKey())) {
175                                         continue;
176                                 }
177
178                                 logger.info(String.format("Removing Chain: %s", loadedChain.getKey()));
179                                 engine.removeReaction(loadedChain.getKey());
180                                 chainsToRemove.add(loadedChain.getKey());
181                         }
182
183                         /* remove removed chains from loaded chains. */
184                         for (String reactionName : chainsToRemove) {
185                                 loadedChains.remove(reactionName);
186                         }
187
188                         /* check for new chains. */
189                         for (Entry<String, Chain> enabledChain : enabledChains.entrySet()) {
190
191                                 /* skip already loaded chains. */
192                                 if (loadedChains.containsValue(enabledChain.getValue())) {
193                                         continue;
194                                 }
195
196                                 logger.info(String.format("Loading new Chain: %s", enabledChain.getKey()));
197
198                                 Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue());
199                                 engine.addReaction(enabledChain.getKey(), reaction);
200                                 loadedChains.put(enabledChain.getKey(), enabledChain.getValue());
201                         }
202
203                         /* wait before checking again. */
204                         Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
205                 }
206         }
207
208         //
209         // STATIC METHODS
210         //
211
212         /**
213          * Parses the given configuration file into a {@link Chain}.
214          *
215          * @param configurationFile
216          *            The configuration file to parse
217          * @return The parsed chain
218          */
219         private static Chain parseConfigurationFile(File configurationFile) {
220                 try {
221                         return objectMapper.readValue(configurationFile, Chain.class);
222                 } catch (JsonParseException jpe1) {
223                         logger.warn(String.format("Could not parse %s.", configurationFile), jpe1);
224                 } catch (JsonMappingException jme1) {
225                         logger.warn(String.format("Could not parse %s.", configurationFile), jme1);
226                 } catch (IOException ioe1) {
227                         logger.info(String.format("Could not read %s.", configurationFile));
228                 }
229                 return null;
230         }
231
232         /**
233          * Extracts the name of the reaction from the given filename.
234          *
235          * @param filename
236          *            The filename to extract the reaction name from
237          * @return The name of the reaction
238          */
239         private static String getReactionName(String filename) {
240                 return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename;
241         }
242
243         public static class ChainDirectory {
244
245                 private final String directory;
246
247                 private ChainDirectory(String directory) {
248                         this.directory = directory;
249                 }
250
251                 public String getDirectory() {
252                         return directory;
253                 }
254
255                 public static ChainDirectory of(String directory) {
256                         return new ChainDirectory(directory);
257                 }
258
259         }
260
261 }