Parse and load watchers from configuration.
[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 net.pterodactylus.rhynodge.Reaction;
31 import net.pterodactylus.rhynodge.engine.Engine;
32 import net.pterodactylus.rhynodge.loader.Chain.Parameter;
33 import net.pterodactylus.rhynodge.loader.Chain.Part;
34
35 import org.apache.log4j.Logger;
36
37 import com.fasterxml.jackson.core.JsonParseException;
38 import com.fasterxml.jackson.databind.JsonMappingException;
39 import com.fasterxml.jackson.databind.ObjectMapper;
40 import com.google.common.base.Predicate;
41 import com.google.common.collect.Maps;
42 import com.google.common.util.concurrent.AbstractExecutionThreadService;
43 import com.google.common.util.concurrent.Uninterruptibles;
44
45 /**
46  * Watches a directory for chain configuration files and loads and unloads
47  * {@link Reaction}s from the {@link Engine}.
48  *
49  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
50  */
51 public class ChainWatcher extends AbstractExecutionThreadService {
52
53         /** The logger. */
54         private static final Logger logger = Logger.getLogger(ChainWatcher.class);
55
56         /** The JSON object mapper. */
57         private static final ObjectMapper objectMapper = new ObjectMapper();
58
59         /** The reaction loader. */
60         private final ReactionLoader reactionLoader = new ReactionLoader();
61
62         /** The engine to load reactions with. */
63         private final Engine engine;
64
65         /** The directory to watch for chain configuration files. */
66         private final String directory;
67
68         /**
69          * Creates a new chain watcher.
70          *
71          * @param engine
72          *            The engine to load reactions with
73          * @param directory
74          *            The directory to watch
75          */
76         public ChainWatcher(Engine engine, String directory) {
77                 this.engine = engine;
78                 this.directory = directory;
79         }
80
81         //
82         // ABSTRACTEXECUTIONTHREADSERVICE METHODS
83         //
84
85         /**
86          * {@inheritDoc}
87          */
88         @Override
89         protected void run() throws Exception {
90
91                 /* loaded chains. */
92                 final Map<String, Chain> loadedChains = new HashMap<String, Chain>();
93
94                 while (isRunning()) {
95
96                         /* check if directory is there. */
97                         File directoryFile = new File(directory);
98                         if (!directoryFile.exists() || !directoryFile.isDirectory() || !directoryFile.canRead()) {
99                                 Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
100                                 continue;
101                         }
102
103                         /* list all files, scan for configuration files. */
104                         logger.debug(String.format("Scanning %s...", directory));
105                         File[] configurationFiles = directoryFile.listFiles(new FilenameFilter() {
106
107                                 @Override
108                                 public boolean accept(File dir, String name) {
109                                         return name.endsWith(".json");
110                                 }
111                         });
112                         logger.debug(String.format("Found %d configuration file(s), parsing...", configurationFiles.length));
113
114                         /* now parse all XML files. */
115                         Map<String, Chain> chains = new HashMap<String, Chain>();
116                         for (File configurationFile : configurationFiles) {
117
118                                 /* parse XML file. */
119                                 Chain chain = parseConfigurationFile(configurationFile);
120                                 if (chain == null) {
121                                         logger.warn(String.format("Could not parse %s.", configurationFile));
122                                         continue;
123                                 }
124
125                                 /* dump chain */
126                                 logger.debug(String.format(" Enabled: %s", chain.enabled()));
127
128                                 if (chain.watcher() != null) {
129                                         logger.debug(String.format("Reaction: %s", chain.watcher().name()));
130                                 } else {
131                                         logger.debug(String.format(" Query: %s", chain.query().name()));
132                                         for (Parameter parameter : chain.query().parameters()) {
133                                                 logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
134                                         }
135                                         for (Part filter : chain.filters()) {
136                                                 logger.debug(String.format(" Filter: %s", filter.name()));
137                                                 for (Parameter parameter : filter.parameters()) {
138                                                         logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
139                                                 }
140                                         }
141                                         logger.debug(String.format(" Trigger: %s", chain.trigger().name()));
142                                         for (Parameter parameter : chain.trigger().parameters()) {
143                                                 logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
144                                         }
145                                 }
146                                 logger.debug(String.format(" Action: %s", chain.action().name()));
147                                 for (Parameter parameter : chain.action().parameters()) {
148                                         logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
149                                 }
150
151                                 chains.put(getReactionName(configurationFile.getName()), chain);
152                         }
153
154                         /* filter enabled chains. */
155                         Map<String, Chain> enabledChains = Maps.filterEntries(chains, new Predicate<Entry<String, Chain>>() {
156
157                                 @Override
158                                 public boolean apply(Entry<String, Chain> chainEntry) {
159                                         return chainEntry.getValue().enabled();
160                                 }
161                         });
162                         logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size()));
163
164                         /* check for removed chains. */
165                         Set<String> chainsToRemove = new HashSet<String>();
166                         for (Entry<String, Chain> loadedChain : loadedChains.entrySet()) {
167
168                                 /* skip chains that still exist. */
169                                 if (enabledChains.containsKey(loadedChain.getKey())) {
170                                         continue;
171                                 }
172
173                                 logger.info(String.format("Removing Chain: %s", loadedChain.getKey()));
174                                 engine.removeReaction(loadedChain.getKey());
175                                 chainsToRemove.add(loadedChain.getKey());
176                         }
177
178                         /* remove removed chains from loaded chains. */
179                         for (String reactionName : chainsToRemove) {
180                                 loadedChains.remove(reactionName);
181                         }
182
183                         /* check for new chains. */
184                         for (Entry<String, Chain> enabledChain : enabledChains.entrySet()) {
185
186                                 /* skip already loaded chains. */
187                                 if (loadedChains.containsValue(enabledChain.getValue())) {
188                                         continue;
189                                 }
190
191                                 logger.info(String.format("Loading new Chain: %s", enabledChain.getKey()));
192
193                                 Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue());
194                                 engine.addReaction(enabledChain.getKey(), reaction);
195                                 loadedChains.put(enabledChain.getKey(), enabledChain.getValue());
196                         }
197
198                         /* wait before checking again. */
199                         Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
200                 }
201         }
202
203         //
204         // STATIC METHODS
205         //
206
207         /**
208          * Parses the given configuration file into a {@link Chain}.
209          *
210          * @param configurationFile
211          *            The configuration file to parse
212          * @return The parsed chain
213          */
214         private static Chain parseConfigurationFile(File configurationFile) {
215                 try {
216                         return objectMapper.readValue(configurationFile, Chain.class);
217                 } catch (JsonParseException jpe1) {
218                         logger.warn(String.format("Could not parse %s.", configurationFile), jpe1);
219                 } catch (JsonMappingException jme1) {
220                         logger.warn(String.format("Could not parse %s.", configurationFile), jme1);
221                 } catch (IOException ioe1) {
222                         logger.info(String.format("Could not read %s.", configurationFile));
223                 }
224                 return null;
225         }
226
227         /**
228          * Extracts the name of the reaction from the given filename.
229          *
230          * @param filename
231          *            The filename to extract the reaction name from
232          * @return The name of the reaction
233          */
234         private static String getReactionName(String filename) {
235                 return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename;
236         }
237
238 }