You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConfigManager.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. /*
  2. * Copyright (c) 2006-2015 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.config;
  23. import com.dmdirc.interfaces.config.AggregateConfigProvider;
  24. import com.dmdirc.interfaces.config.ConfigChangeListener;
  25. import com.dmdirc.interfaces.config.ConfigProvider;
  26. import com.dmdirc.interfaces.config.ConfigProviderMigrator;
  27. import com.dmdirc.util.ClientInfo;
  28. import com.dmdirc.util.validators.Validator;
  29. import com.google.common.collect.ArrayListMultimap;
  30. import com.google.common.collect.Multimap;
  31. import java.util.ArrayList;
  32. import java.util.Collection;
  33. import java.util.Collections;
  34. import java.util.HashMap;
  35. import java.util.HashSet;
  36. import java.util.List;
  37. import java.util.Map;
  38. import java.util.Set;
  39. import java.util.TreeMap;
  40. import org.slf4j.Logger;
  41. import org.slf4j.LoggerFactory;
  42. /**
  43. * The config manager manages the various config sources for each entity.
  44. */
  45. class ConfigManager implements ConfigChangeListener, ConfigProviderListener,
  46. AggregateConfigProvider {
  47. private static final Logger LOG = LoggerFactory.getLogger(ConfigManager.class);
  48. /** Temporary map for lookup stats. */
  49. private static final Map<String, Integer> STATS = new TreeMap<>();
  50. /** Magical domain to redirect to the version identity. */
  51. private static final String VERSION_DOMAIN = "version";
  52. /** A list of sources for this config manager. */
  53. private final List<ConfigFileBackedConfigProvider> sources = new ArrayList<>();
  54. /** The listeners registered for this manager. */
  55. private final Multimap<String, ConfigChangeListener> listeners = ArrayListMultimap.create();
  56. /** The config binder to use for this manager. */
  57. private final ConfigBinder binder;
  58. /** The manager to use to fetch global state. */
  59. private final IdentityManager manager;
  60. /** Client info object. */
  61. private final ClientInfo clientInfo;
  62. /** The protocol this manager is for. */
  63. private String protocol;
  64. /** The ircd this manager is for. */
  65. private String ircd;
  66. /** The network this manager is for. */
  67. private String network;
  68. /** The server this manager is for. */
  69. private String server;
  70. /** The channel this manager is for. */
  71. private String channel;
  72. /**
  73. * Creates a new instance of ConfigManager.
  74. *
  75. * @param manager The manager to use to retrieve global state horribly.
  76. * @param protocol The protocol for this manager
  77. * @param ircd The name of the ircd for this manager
  78. * @param network The name of the network for this manager
  79. * @param server The name of the server for this manager
  80. *
  81. * @since 0.6.3
  82. */
  83. ConfigManager(
  84. final ClientInfo clientInfo,
  85. final IdentityManager manager,
  86. final String protocol, final String ircd,
  87. final String network, final String server) {
  88. this(clientInfo, manager, protocol, ircd, network, server, "<Unknown>");
  89. }
  90. /**
  91. * Creates a new instance of ConfigManager.
  92. *
  93. * @param manager The manager to use to retrieve global state horribly.
  94. * @param protocol The protocol for this manager
  95. * @param ircd The name of the ircd for this manager
  96. * @param network The name of the network for this manager
  97. * @param server The name of the server for this manager
  98. * @param channel The name of the channel for this manager
  99. *
  100. * @since 0.6.3
  101. */
  102. ConfigManager(
  103. final ClientInfo clientInfo,
  104. final IdentityManager manager,
  105. final String protocol, final String ircd,
  106. final String network, final String server, final String channel) {
  107. final String chanName = channel + '@' + network;
  108. this.clientInfo = clientInfo;
  109. this.manager = manager;
  110. this.protocol = protocol;
  111. this.ircd = ircd;
  112. this.network = network;
  113. this.server = server;
  114. this.channel = chanName;
  115. binder = new ConfigBinder(this);
  116. }
  117. @Override
  118. public ConfigBinder getBinder() {
  119. return binder;
  120. }
  121. @Override
  122. public String getOption(final String domain, final String option,
  123. final Validator<String> validator) {
  124. doStats(domain, option);
  125. if (VERSION_DOMAIN.equals(domain)) {
  126. final String response = clientInfo.getVersionConfigSetting(VERSION_DOMAIN, option);
  127. if (response == null || validator.validate(response).isFailure()) {
  128. return null;
  129. }
  130. return response;
  131. }
  132. synchronized (sources) {
  133. for (ConfigProvider source : sources) {
  134. if (source.hasOption(domain, option, validator)) {
  135. return source.getOption(domain, option, validator);
  136. }
  137. }
  138. }
  139. return null;
  140. }
  141. @Override
  142. public boolean hasOption(final String domain, final String option,
  143. final Validator<String> validator) {
  144. doStats(domain, option);
  145. if (VERSION_DOMAIN.equals(domain)) {
  146. final String response = clientInfo.getVersionConfigSetting(VERSION_DOMAIN, option);
  147. return response != null && !validator.validate(response).isFailure();
  148. }
  149. synchronized (sources) {
  150. for (ConfigProvider source : sources) {
  151. if (source.hasOption(domain, option, validator)) {
  152. return true;
  153. }
  154. }
  155. }
  156. return false;
  157. }
  158. @Override
  159. public Map<String, String> getOptions(final String domain) {
  160. if (VERSION_DOMAIN.equals(domain)) {
  161. return manager.getVersionSettings().getOptions(domain);
  162. }
  163. final Map<String, String> res = new HashMap<>();
  164. synchronized (sources) {
  165. for (int i = sources.size() - 1; i >= 0; i--) {
  166. res.putAll(sources.get(i).getOptions(domain));
  167. }
  168. }
  169. return res;
  170. }
  171. /**
  172. * Removes the specified identity from this manager.
  173. *
  174. * @param identity The identity to be removed
  175. */
  176. public void removeIdentity(final ConfigProvider identity) {
  177. if (!sources.contains(identity)) {
  178. return;
  179. }
  180. final Collection<String[]> changed = new ArrayList<>();
  181. // Determine which settings will have changed
  182. for (String domain : identity.getDomains()) {
  183. identity.getOptions(domain).keySet().stream()
  184. .filter(option -> identity.equals(getScope(domain, option)))
  185. .forEach(option -> changed.add(new String[]{domain, option}));
  186. }
  187. synchronized (sources) {
  188. identity.removeListener(this);
  189. sources.remove(identity);
  190. }
  191. // Fire change listeners
  192. for (String[] setting : changed) {
  193. configChanged(setting[0], setting[1]);
  194. }
  195. }
  196. /**
  197. * Retrieves the identity that currently defines the specified domain and option.
  198. *
  199. * @param domain The domain to search for
  200. * @param option The option to search for
  201. *
  202. * @return The identity that defines that setting, or null on failure
  203. */
  204. protected ConfigProvider getScope(final String domain, final String option) {
  205. if (VERSION_DOMAIN.equals(domain)) {
  206. return manager.getVersionSettings();
  207. }
  208. synchronized (sources) {
  209. for (ConfigProvider source : sources) {
  210. if (source.hasOptionString(domain, option)) {
  211. return source;
  212. }
  213. }
  214. }
  215. return null;
  216. }
  217. /**
  218. * Checks whether the specified identity applies to this config manager.
  219. *
  220. * @param identity The identity to test
  221. *
  222. * @return True if the identity applies, false otherwise
  223. */
  224. public boolean identityApplies(final ConfigFileBackedConfigProvider identity) {
  225. final String comp;
  226. switch (identity.getTarget().getType()) {
  227. case PROTOCOL:
  228. comp = protocol;
  229. break;
  230. case IRCD:
  231. comp = ircd;
  232. break;
  233. case NETWORK:
  234. comp = network;
  235. break;
  236. case SERVER:
  237. comp = server;
  238. break;
  239. case CHANNEL:
  240. comp = channel;
  241. break;
  242. case CUSTOM:
  243. // We don't want custom identities
  244. comp = null;
  245. break;
  246. default:
  247. comp = "";
  248. break;
  249. }
  250. final boolean result = comp != null
  251. && identityTargetMatches(identity.getTarget().getData(), comp);
  252. LOG.trace("Checking if identity {} applies. Comparison: {}, target: {}, result: {}",
  253. identity, comp, identity.getTarget().getData(), result);
  254. return result;
  255. }
  256. /**
  257. * Determines whether the specified identity target matches the desired target. If the desired
  258. * target is prefixed with "re:", it is treated as a regular expression; otherwise the strings
  259. * are compared lexicographically to determine a match.
  260. *
  261. * @param desired The target string required by this config manager
  262. * @param actual The target string supplied by the identity
  263. *
  264. * @return True if the identity should be applied, false otherwise
  265. *
  266. * @since 0.6.3m2
  267. */
  268. protected boolean identityTargetMatches(final String actual, final String desired) {
  269. return actual.startsWith("re:") ? desired.matches(actual.substring(3))
  270. : actual.equalsIgnoreCase(desired);
  271. }
  272. /**
  273. * Called whenever there is a new identity available. Checks if the identity is relevant for
  274. * this manager, and adds it if it is.
  275. *
  276. * @param identity The identity to be checked
  277. */
  278. public void checkIdentity(final ConfigFileBackedConfigProvider identity) {
  279. if (!sources.contains(identity) && identityApplies(identity)) {
  280. synchronized (sources) {
  281. sources.add(identity);
  282. identity.addListener(this);
  283. Collections.sort(sources, new ConfigProviderTargetComparator());
  284. }
  285. // Determine which settings will have changed
  286. for (String domain : identity.getDomains()) {
  287. identity.getOptions(domain).keySet().stream()
  288. .filter(option -> identity.equals(getScope(domain, option)))
  289. .forEach(option -> configChanged(domain, option));
  290. }
  291. }
  292. }
  293. @Override
  294. public Set<String> getDomains() {
  295. final Set<String> res = new HashSet<>();
  296. synchronized (sources) {
  297. for (ConfigProvider source : sources) {
  298. res.addAll(source.getDomains());
  299. }
  300. }
  301. return res;
  302. }
  303. @Override
  304. public List<ConfigProvider> getSources() {
  305. return new ArrayList<>(sources);
  306. }
  307. /**
  308. * Migrates this manager from its current configuration to the appropriate one for the specified
  309. * new parameters, firing listeners where settings have changed.
  310. *
  311. * <p>
  312. * This is package private - only callers with access to a {@link ConfigProviderMigrator}
  313. * should be able to migrate managers.
  314. *
  315. * @param protocol The protocol for this manager
  316. * @param ircd The new name of the ircd for this manager
  317. * @param network The new name of the network for this manager
  318. * @param server The new name of the server for this manager
  319. * @param channel The new name of the channel for this manager
  320. */
  321. void migrate(final String protocol, final String ircd,
  322. final String network, final String server, final String channel) {
  323. LOG.debug("Migrating from {{}, {}, {}, {}, {}} to {{}, {}, {}, {}, {}}", this.protocol,
  324. this.ircd, this.network, this.server, this.channel, protocol, ircd, network, server,
  325. channel);
  326. this.protocol = protocol;
  327. this.ircd = ircd;
  328. this.network = network;
  329. this.server = server;
  330. this.channel = channel + '@' + network;
  331. new ArrayList<>(sources).stream().filter(identity -> !identityApplies(identity))
  332. .forEach(identity -> {
  333. LOG.debug("Removing identity that no longer applies: {}", identity);
  334. removeIdentity(identity);
  335. });
  336. final List<ConfigFileBackedConfigProvider> newSources = manager.getIdentitiesForManager(this);
  337. for (ConfigFileBackedConfigProvider identity : newSources) {
  338. LOG.trace("Testing new identity: {}", identity);
  339. checkIdentity(identity);
  340. }
  341. LOG.debug("New identities: {}", sources);
  342. }
  343. /**
  344. * Records the lookup request for the specified domain and option.
  345. *
  346. * @param domain The domain that is being looked up
  347. * @param option The option that is being looked up
  348. */
  349. @SuppressWarnings("PMD.AvoidCatchingNPE")
  350. protected static void doStats(final String domain, final String option) {
  351. final String key = domain + '.' + option;
  352. try {
  353. STATS.put(key, 1 + (STATS.containsKey(key) ? STATS.get(key) : 0));
  354. } catch (NullPointerException ex) {
  355. // JVM bugs ftl.
  356. }
  357. }
  358. /**
  359. * Retrieves the statistic map.
  360. *
  361. * @return A map of config options to lookup counts
  362. */
  363. public static Map<String, Integer> getStats() {
  364. return STATS;
  365. }
  366. @Override
  367. public void addChangeListener(final String domain,
  368. final ConfigChangeListener listener) {
  369. addListener(domain, listener);
  370. }
  371. @Override
  372. public void addChangeListener(final String domain, final String key,
  373. final ConfigChangeListener listener) {
  374. addListener(domain + '.' + key, listener);
  375. }
  376. @Override
  377. public void removeListener(final ConfigChangeListener listener) {
  378. synchronized (listeners) {
  379. final Iterable<String> keys = new HashSet<>(listeners.keySet());
  380. keys.forEach(k -> listeners.remove(k, listener));
  381. }
  382. }
  383. /**
  384. * Adds the specified listener to the internal map/list.
  385. *
  386. * @param key The key to use (domain or domain.key)
  387. * @param listener The listener to register
  388. */
  389. private void addListener(final String key,
  390. final ConfigChangeListener listener) {
  391. synchronized (listeners) {
  392. listeners.put(key, listener);
  393. }
  394. }
  395. @Override
  396. public void configChanged(final String domain, final String key) {
  397. final Collection<ConfigChangeListener> targets = new ArrayList<>();
  398. if (listeners.containsKey(domain)) {
  399. targets.addAll(listeners.get(domain));
  400. }
  401. if (listeners.containsKey(domain + '.' + key)) {
  402. targets.addAll(listeners.get(domain + '.' + key));
  403. }
  404. for (ConfigChangeListener listener : targets) {
  405. listener.configChanged(domain, key);
  406. }
  407. }
  408. @Override
  409. public void configProviderAdded(final ConfigFileBackedConfigProvider configProvider) {
  410. checkIdentity(configProvider);
  411. }
  412. @Override
  413. public void configProviderRemoved(final ConfigFileBackedConfigProvider configProvider) {
  414. removeIdentity(configProvider);
  415. }
  416. }