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

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