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.

ActionManager.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /*
  2. * Copyright (c) 2006-2013 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.actions;
  23. import com.dmdirc.Precondition;
  24. import com.dmdirc.ServerManager;
  25. import com.dmdirc.actions.internal.WhoisNumericFormatter;
  26. import com.dmdirc.config.ConfigBinding;
  27. import com.dmdirc.interfaces.ActionController;
  28. import com.dmdirc.interfaces.ActionListener;
  29. import com.dmdirc.interfaces.actions.ActionComparison;
  30. import com.dmdirc.interfaces.actions.ActionComponent;
  31. import com.dmdirc.interfaces.actions.ActionType;
  32. import com.dmdirc.interfaces.config.IdentityController;
  33. import com.dmdirc.logger.ErrorLevel;
  34. import com.dmdirc.logger.Logger;
  35. import com.dmdirc.updater.components.ActionGroupComponent;
  36. import com.dmdirc.updater.manager.UpdateManager;
  37. import com.dmdirc.util.collections.MapList;
  38. import com.dmdirc.util.resourcemanager.ZipResourceManager;
  39. import java.io.File;
  40. import java.io.IOException;
  41. import java.util.ArrayList;
  42. import java.util.Collections;
  43. import java.util.HashMap;
  44. import java.util.List;
  45. import java.util.Map;
  46. import java.util.Set;
  47. import javax.inject.Provider;
  48. import lombok.extern.slf4j.Slf4j;
  49. import static com.google.common.base.Preconditions.checkArgument;
  50. import static com.google.common.base.Preconditions.checkNotNull;
  51. /**
  52. * Manages all actions for the client.
  53. */
  54. @Slf4j
  55. public class ActionManager implements ActionController {
  56. /** The ActionManager Instance. */
  57. private static ActionManager me;
  58. /** The identity manager to load configuration from. */
  59. private final IdentityController identityManager;
  60. /** The ServerManager currently in use. */
  61. private final ServerManager serverManager;
  62. /** The factory to use to create actions. */
  63. private final ActionFactory factory;
  64. /** Provider for action wrappers. */
  65. private final Provider<Set<ActionGroup>> actionWrappersProvider;
  66. /** Provider of an update manager. */
  67. private final Provider<UpdateManager> updateManagerProvider;
  68. /** A list of registered action types. */
  69. private final List<ActionType> types = new ArrayList<>();
  70. /** A list of registered action components. */
  71. private final List<ActionComponent> components = new ArrayList<>();
  72. /** A list of registered action comparisons. */
  73. private final List<ActionComparison> comparisons = new ArrayList<>();
  74. /** A map linking types and a list of actions that're registered for them. */
  75. private final MapList<ActionType, Action> actions = new MapList<>();
  76. /** A map linking groups and a list of actions that're in them. */
  77. private final Map<String, ActionGroup> groups = new HashMap<>();
  78. /** A map of objects to synchronise on for concurrency groups. */
  79. private final Map<String, Object> locks = new HashMap<>();
  80. /** A map of the action type groups to the action types within. */
  81. private final MapList<String, ActionType> typeGroups = new MapList<>();
  82. /** The listeners that we have registered. */
  83. private final MapList<ActionType, ActionListener> listeners = new MapList<>();
  84. /** Indicates whether or not user actions should be killed (not processed). */
  85. @ConfigBinding(domain="actions", key="killswitch")
  86. private boolean killSwitch;
  87. /**
  88. * Creates a new instance of ActionManager.
  89. *
  90. * @param serverManager The ServerManager in use.
  91. * @param identityManager The IdentityManager to load configuration from.
  92. * @param factory The factory to use to create new actions.
  93. * @param actionWrappersProvider Provider of action wrappers.
  94. * @param updateManagerProvider Provider of an update manager, to register components.
  95. */
  96. public ActionManager(
  97. final ServerManager serverManager,
  98. final IdentityController identityManager,
  99. final ActionFactory factory,
  100. final Provider<Set<ActionGroup>> actionWrappersProvider,
  101. final Provider<UpdateManager> updateManagerProvider) {
  102. this.serverManager = serverManager;
  103. this.identityManager = identityManager;
  104. this.factory = factory;
  105. this.actionWrappersProvider = actionWrappersProvider;
  106. this.updateManagerProvider = updateManagerProvider;
  107. }
  108. /**
  109. * Get the server manager.
  110. *
  111. * @return ServerManager
  112. */
  113. public ServerManager getServerManager() {
  114. return serverManager;
  115. }
  116. /**
  117. * Create the singleton instance of the Action Manager.
  118. *
  119. * @param actionManager The manager to return for calls to {@link #getActionManager()}.
  120. */
  121. @Deprecated
  122. public static void setActionManager(final ActionManager actionManager) {
  123. me = actionManager;
  124. }
  125. /**
  126. * Returns a singleton instance of the Action Manager.
  127. *
  128. * @return A singleton ActionManager instance
  129. */
  130. public static ActionManager getActionManager() {
  131. return me;
  132. }
  133. /**
  134. * Initialiases the actions manager.
  135. *
  136. * @param colourComparisons The colour comparisons to use.
  137. */
  138. // TODO: Refactor to take a list of comparisons/sources.
  139. public void initialise(final ColourActionComparison colourComparisons) {
  140. log.info("Initialising the actions manager");
  141. identityManager.getGlobalConfiguration().getBinder().bind(this, ActionManager.class);
  142. registerTypes(CoreActionType.values());
  143. registerComparisons(CoreActionComparison.values());
  144. registerComparisons(colourComparisons.getComparisons());
  145. registerComponents(CoreActionComponent.values());
  146. for (ActionGroup wrapper : actionWrappersProvider.get()) {
  147. addGroup(wrapper);
  148. }
  149. new WhoisNumericFormatter(identityManager.getAddonSettings()).register();
  150. // Register a listener for the closing event, so we can save actions
  151. registerListener(new ActionListener() {
  152. /** {@inheritDoc} */
  153. @Override
  154. public void processEvent(final ActionType type, final StringBuffer format,
  155. final Object... arguments) {
  156. saveAllActions();
  157. }
  158. }, CoreActionType.CLIENT_CLOSED);
  159. }
  160. /** {@inheritDoc} */
  161. @Override
  162. public void saveAllActions() {
  163. for (ActionGroup group : groups.values()) {
  164. for (Action action : group) {
  165. action.save();
  166. }
  167. }
  168. }
  169. /** {@inheritDoc} */
  170. @Override
  171. public void registerSetting(final String name, final String value) {
  172. log.debug("Registering new action setting: {} = {}", name, value);
  173. identityManager.getAddonSettings().setOption("actions", name, value);
  174. }
  175. /** {@inheritDoc} */
  176. @Override
  177. public void addGroup(final ActionGroup group) {
  178. groups.put(group.getName(), group);
  179. }
  180. /** {@inheritDoc} */
  181. @Override
  182. public void registerTypes(final ActionType[] newTypes) {
  183. for (ActionType type : newTypes) {
  184. checkNotNull(type);
  185. if (!types.contains(type)) {
  186. log.debug("Registering action type: {}", type);
  187. types.add(type);
  188. typeGroups.add(type.getType().getGroup(), type);
  189. }
  190. }
  191. }
  192. /** {@inheritDoc} */
  193. @Override
  194. public void registerComponents(final ActionComponent[] comps) {
  195. for (ActionComponent comp : comps) {
  196. checkNotNull(comp);
  197. log.debug("Registering action component: {}", comp);
  198. components.add(comp);
  199. }
  200. }
  201. /** {@inheritDoc} */
  202. @Override
  203. public void registerComparisons(final ActionComparison[] comps) {
  204. for (ActionComparison comp : comps) {
  205. checkNotNull(comp);
  206. log.debug("Registering action comparison: {}", comp);
  207. comparisons.add(comp);
  208. }
  209. }
  210. /** {@inheritDoc} */
  211. @Override
  212. public Map<String, ActionGroup> getGroupsMap() {
  213. return Collections.unmodifiableMap(groups);
  214. }
  215. /** {@inheritDoc} */
  216. @Override
  217. public MapList<String, ActionType> getGroupedTypes() {
  218. return new MapList<>(typeGroups);
  219. }
  220. /** {@inheritDoc} */
  221. @Override
  222. public void loadUserActions() {
  223. actions.clear();
  224. for (ActionGroup group : groups.values()) {
  225. group.clear();
  226. }
  227. final File dir = new File(getDirectory());
  228. if (!dir.exists()) {
  229. try {
  230. dir.mkdirs();
  231. dir.createNewFile();
  232. } catch (IOException ex) {
  233. Logger.userError(ErrorLevel.HIGH, "I/O error when creating actions directory: "
  234. + ex.getMessage());
  235. }
  236. }
  237. if (dir.listFiles() == null) {
  238. Logger.userError(ErrorLevel.MEDIUM, "Unable to load user action files");
  239. } else {
  240. for (File file : dir.listFiles()) {
  241. if (file.isDirectory()) {
  242. loadActions(file);
  243. }
  244. }
  245. }
  246. registerComponents(updateManagerProvider.get());
  247. }
  248. /**
  249. * Creates new ActionGroupComponents for each action group.
  250. *
  251. * @param updateManager The update manager to register components with
  252. */
  253. private void registerComponents(final UpdateManager updateManager) {
  254. for (ActionGroup group : groups.values()) {
  255. if (group.getComponent() != -1 && group.getVersion() != null) {
  256. updateManager.addComponent(new ActionGroupComponent(group));
  257. }
  258. }
  259. }
  260. /**
  261. * Loads action files from a specified group directory.
  262. *
  263. * @param dir The directory to scan.
  264. */
  265. @Precondition("The specified File is not null and represents a directory")
  266. private void loadActions(final File dir) {
  267. checkNotNull(dir);
  268. checkArgument(dir.isDirectory());
  269. log.debug("Loading actions from directory: {}", dir.getAbsolutePath());
  270. if (!groups.containsKey(dir.getName())) {
  271. groups.put(dir.getName(), new ActionGroup(dir.getName()));
  272. }
  273. for (File file : dir.listFiles()) {
  274. factory.getAction(dir.getName(), file.getName());
  275. }
  276. }
  277. /** {@inheritDoc} */
  278. @Override
  279. public void addAction(final Action action) {
  280. checkNotNull(action);
  281. log.debug("Registering action: {}/{} (status: {})",
  282. new Object[] { action.getGroup(), action.getName(), action.getStatus() });
  283. if (action.getStatus() != ActionStatus.FAILED) {
  284. for (ActionType trigger : action.getTriggers()) {
  285. log.trace("Action has trigger {}", trigger);
  286. actions.add(trigger, action);
  287. }
  288. }
  289. getOrCreateGroup(action.getGroup()).add(action);
  290. }
  291. /** {@inheritDoc} */
  292. @Override
  293. public ActionGroup getOrCreateGroup(final String name) {
  294. if (!groups.containsKey(name)) {
  295. groups.put(name, new ActionGroup(name));
  296. }
  297. return groups.get(name);
  298. }
  299. /** {@inheritDoc} */
  300. @Override
  301. public void removeAction(final Action action) {
  302. checkNotNull(action);
  303. actions.removeFromAll(action);
  304. getOrCreateGroup(action.getGroup()).remove(action);
  305. }
  306. /** {@inheritDoc} */
  307. @Override
  308. public void reregisterAction(final Action action) {
  309. removeAction(action);
  310. addAction(action);
  311. }
  312. /** {@inheritDoc} */
  313. @Override
  314. public boolean triggerEvent(final ActionType type,
  315. final StringBuffer format, final Object ... arguments) {
  316. checkNotNull(type);
  317. checkNotNull(type.getType());
  318. checkArgument(type.getType().getArity() == arguments.length);
  319. log.trace("Calling listeners for event of type {}", type);
  320. boolean res = false;
  321. if (listeners.containsKey(type)) {
  322. for (ActionListener listener : new ArrayList<>(listeners.get(type))) {
  323. try {
  324. listener.processEvent(type, format, arguments);
  325. } catch (Exception e) {
  326. Logger.appError(ErrorLevel.MEDIUM, "Error processing action: "
  327. + e.getMessage(), e);
  328. }
  329. }
  330. }
  331. if (!killSwitch) {
  332. res |= triggerActions(type, format, arguments);
  333. }
  334. return !res;
  335. }
  336. /**
  337. * Triggers actions that respond to the specified type.
  338. *
  339. * @param type The type of the event to process
  340. * @param format The format of the message that's going to be displayed for
  341. * the event. Actions may change this format.
  342. * @param arguments The arguments for the event
  343. * @return True if the event should be skipped, or false if it can continue
  344. */
  345. @Precondition("The specified ActionType is not null")
  346. private boolean triggerActions(final ActionType type,
  347. final StringBuffer format, final Object ... arguments) {
  348. checkNotNull(type);
  349. boolean res = false;
  350. log.trace("Executing actions for event of type {}", type);
  351. if (actions.containsKey(type)) {
  352. for (Action action : new ArrayList<>(actions.get(type))) {
  353. try {
  354. if (action.getConcurrencyGroup() == null) {
  355. res |= action.trigger(getServerManager(), format, arguments);
  356. } else {
  357. synchronized (locks) {
  358. if (!locks.containsKey(action.getConcurrencyGroup())) {
  359. locks.put(action.getConcurrencyGroup(), new Object());
  360. }
  361. }
  362. synchronized (locks.get(action.getConcurrencyGroup())) {
  363. res |= action.trigger(getServerManager(), format, arguments);
  364. }
  365. }
  366. } catch (LinkageError | Exception e) {
  367. Logger.appError(ErrorLevel.MEDIUM, "Error processing action: "
  368. + e.getMessage(), e);
  369. }
  370. }
  371. }
  372. return res;
  373. }
  374. /**
  375. * Returns the directory that should be used to store actions.
  376. *
  377. * @return The directory that should be used to store actions
  378. */
  379. public String getDirectory() {
  380. return this.identityManager.getConfigurationDirectory() + "actions" + System.getProperty("file.separator");
  381. }
  382. /** {@inheritDoc} */
  383. @Override
  384. public ActionGroup createGroup(final String group) {
  385. checkNotNull(group);
  386. checkArgument(!group.isEmpty());
  387. checkArgument(!groups.containsKey(group));
  388. final File file = new File(getDirectory() + group);
  389. if (file.isDirectory() || file.mkdir()) {
  390. final ActionGroup actionGroup = new ActionGroup(group);
  391. groups.put(group, actionGroup);
  392. return actionGroup;
  393. } else {
  394. throw new IllegalArgumentException("Unable to create action group directory"
  395. + "\n\nDir: " + getDirectory() + group);
  396. }
  397. }
  398. /** {@inheritDoc} */
  399. @Override
  400. public void deleteGroup(final String group) {
  401. checkNotNull(group);
  402. checkArgument(!group.isEmpty());
  403. checkArgument(groups.containsKey(group));
  404. for (Action action : groups.get(group).getActions()) {
  405. removeAction(action);
  406. }
  407. final File dir = new File(getDirectory() + group);
  408. if (dir.isDirectory()) {
  409. for (File file : dir.listFiles()) {
  410. if (!file.delete()) {
  411. Logger.userError(ErrorLevel.MEDIUM, "Unable to remove file: "
  412. + file.getAbsolutePath());
  413. return;
  414. }
  415. }
  416. }
  417. if (!dir.delete()) {
  418. Logger.userError(ErrorLevel.MEDIUM, "Unable to remove directory: "
  419. + dir.getAbsolutePath());
  420. return;
  421. }
  422. groups.remove(group);
  423. }
  424. /** {@inheritDoc} */
  425. @Override
  426. public void changeGroupName(final String oldName, final String newName) {
  427. checkNotNull(oldName);
  428. checkArgument(!oldName.isEmpty());
  429. checkNotNull(newName);
  430. checkArgument(!newName.isEmpty());
  431. checkArgument(groups.containsKey(oldName));
  432. checkArgument(!groups.containsKey(newName));
  433. checkArgument(!newName.equals(oldName));
  434. createGroup(newName);
  435. for (Action action : groups.get(oldName).getActions()) {
  436. action.setGroup(newName);
  437. getOrCreateGroup(oldName).remove(action);
  438. getOrCreateGroup(newName).add(action);
  439. }
  440. deleteGroup(oldName);
  441. }
  442. /** {@inheritDoc} */
  443. @Override
  444. public ActionType getType(final String type) {
  445. if (type == null || type.isEmpty()) {
  446. return null;
  447. }
  448. for (ActionType target : types) {
  449. if (target.name().equals(type)) {
  450. return target;
  451. }
  452. }
  453. return null;
  454. }
  455. /** {@inheritDoc} */
  456. @Override
  457. public List<ActionType> findCompatibleTypes(final ActionType type) {
  458. checkNotNull(type);
  459. final List<ActionType> res = new ArrayList<>();
  460. for (ActionType target : types) {
  461. if (!target.equals(type) && target.getType().equals(type.getType())) {
  462. res.add(target);
  463. }
  464. }
  465. return res;
  466. }
  467. /** {@inheritDoc} */
  468. @Override
  469. public List<ActionComponent> findCompatibleComponents(final Class<?> target) {
  470. checkNotNull(target);
  471. final List<ActionComponent> res = new ArrayList<>();
  472. for (ActionComponent subject : components) {
  473. if (subject.appliesTo().equals(target)) {
  474. res.add(subject);
  475. }
  476. }
  477. return res;
  478. }
  479. /** {@inheritDoc} */
  480. @Override
  481. public List<ActionComparison> findCompatibleComparisons(final Class<?> target) {
  482. checkNotNull(target);
  483. final List<ActionComparison> res = new ArrayList<>();
  484. for (ActionComparison subject : comparisons) {
  485. if (subject.appliesTo().equals(target)) {
  486. res.add(subject);
  487. }
  488. }
  489. return res;
  490. }
  491. /** {@inheritDoc} */
  492. @Override
  493. public ActionComponent getComponent(final String type) {
  494. checkNotNull(type);
  495. checkArgument(!type.isEmpty());
  496. for (ActionComponent target : components) {
  497. if (target.name().equals(type)) {
  498. return target;
  499. }
  500. }
  501. return null;
  502. }
  503. /** {@inheritDoc} */
  504. @Override
  505. public ActionComparison getComparison(final String type) {
  506. checkNotNull(type);
  507. checkArgument(!type.isEmpty());
  508. for (ActionComparison target : comparisons) {
  509. if (target.name().equals(type)) {
  510. return target;
  511. }
  512. }
  513. return null;
  514. }
  515. /**
  516. * Installs an action pack located at the specified path.
  517. *
  518. * @param path The full path of the action pack .zip.
  519. * @throws IOException If the zip cannot be extracted
  520. */
  521. public static void installActionPack(final String path) throws IOException {
  522. final ZipResourceManager ziprm = ZipResourceManager.getInstance(path);
  523. ziprm.extractResources("", getActionManager().getDirectory());
  524. getActionManager().loadUserActions();
  525. new File(path).delete();
  526. }
  527. /** {@inheritDoc} */
  528. @Override
  529. public void registerListener(final ActionListener listener, final ActionType ... types) {
  530. for (ActionType type : types) {
  531. listeners.add(type, listener);
  532. }
  533. }
  534. /** {@inheritDoc} */
  535. @Override
  536. public void unregisterListener(final ActionListener listener, final ActionType ... types) {
  537. for (ActionType type : types) {
  538. listeners.remove(type, listener);
  539. }
  540. }
  541. /** {@inheritDoc} */
  542. @Override
  543. public void unregisterListener(final ActionListener listener) {
  544. listeners.removeFromAll(listener);
  545. }
  546. }