2 * Rhynodge - ChainWatcher.java - Copyright © 2013 David Roden
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.
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.
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/>.
18 package net.pterodactylus.rhynodge.loader;
21 import java.io.FilenameFilter;
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.HashSet;
26 import java.util.Map.Entry;
28 import java.util.concurrent.TimeUnit;
30 import javax.inject.Inject;
31 import javax.inject.Singleton;
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;
38 import org.apache.log4j.Logger;
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;
49 * Watches a directory for chain configuration files and loads and unloads
50 * {@link Reaction}s from the {@link Engine}.
52 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
55 public class ChainWatcher extends AbstractExecutionThreadService {
58 private static final Logger logger = Logger.getLogger(ChainWatcher.class);
60 /** The JSON object mapper. */
61 private static final ObjectMapper objectMapper = new ObjectMapper();
63 /** The reaction loader. */
64 private final ReactionLoader reactionLoader = new ReactionLoader();
66 /** The engine to load reactions with. */
67 private final Engine engine;
69 /** The directory to watch for chain configuration files. */
70 private final String directory;
73 * Creates a new chain watcher.
76 * The engine to load reactions with
78 * The directory to watch
81 public ChainWatcher(Engine engine, ChainDirectory directory) {
83 this.directory = directory.getDirectory();
87 // ABSTRACTEXECUTIONTHREADSERVICE METHODS
94 protected void run() throws Exception {
97 final Map<String, Chain> loadedChains = new HashMap<String, Chain>();
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);
108 /* list all files, scan for configuration files. */
109 logger.debug(String.format("Scanning %s...", directory));
110 File[] configurationFiles = directoryFile.listFiles(new FilenameFilter() {
113 public boolean accept(File dir, String name) {
114 return name.endsWith(".json");
117 logger.debug(String.format("Found %d configuration file(s), parsing...", configurationFiles.length));
119 /* now parse all XML files. */
120 Map<String, Chain> chains = new HashMap<String, Chain>();
121 for (File configurationFile : configurationFiles) {
123 /* parse XML file. */
124 Chain chain = parseConfigurationFile(configurationFile);
126 logger.warn(String.format("Could not parse %s.", configurationFile));
131 logger.debug(String.format(" Enabled: %s", chain.enabled()));
133 if (chain.watcher() != null) {
134 logger.debug(String.format("Reaction: %s", chain.watcher().name()));
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()));
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()));
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()));
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()));
156 chains.put(getReactionName(configurationFile.getName()), chain);
159 /* filter enabled chains. */
160 Map<String, Chain> enabledChains = Maps.filterEntries(chains, new Predicate<Entry<String, Chain>>() {
163 public boolean apply(Entry<String, Chain> chainEntry) {
164 return chainEntry.getValue().enabled();
167 logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size()));
169 /* check for removed chains. */
170 Set<String> chainsToRemove = new HashSet<String>();
171 for (Entry<String, Chain> loadedChain : loadedChains.entrySet()) {
173 /* skip chains that still exist. */
174 if (enabledChains.containsKey(loadedChain.getKey())) {
178 logger.info(String.format("Removing Chain: %s", loadedChain.getKey()));
179 engine.removeReaction(loadedChain.getKey());
180 chainsToRemove.add(loadedChain.getKey());
183 /* remove removed chains from loaded chains. */
184 for (String reactionName : chainsToRemove) {
185 loadedChains.remove(reactionName);
188 /* check for new chains. */
189 for (Entry<String, Chain> enabledChain : enabledChains.entrySet()) {
191 /* skip already loaded chains. */
192 if (loadedChains.containsValue(enabledChain.getValue())) {
196 logger.info(String.format("Loading new Chain: %s", enabledChain.getKey()));
198 Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue());
199 engine.addReaction(enabledChain.getKey(), reaction);
200 loadedChains.put(enabledChain.getKey(), enabledChain.getValue());
203 /* wait before checking again. */
204 Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
213 * Parses the given configuration file into a {@link Chain}.
215 * @param configurationFile
216 * The configuration file to parse
217 * @return The parsed chain
219 private static Chain parseConfigurationFile(File configurationFile) {
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));
233 * Extracts the name of the reaction from the given filename.
236 * The filename to extract the reaction name from
237 * @return The name of the reaction
239 private static String getReactionName(String filename) {
240 return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename;
243 public static class ChainDirectory {
245 private final String directory;
247 private ChainDirectory(String directory) {
248 this.directory = directory;
251 public String getDirectory() {
255 public static ChainDirectory of(String directory) {
256 return new ChainDirectory(directory);