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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. /*
  2. * Copyright (c) 2006-2014 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 org.slf4j.LoggerFactory;
  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. public class ActionManager implements ActionController {
  55. private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ActionManager.class);
  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. /** The directory to load and save actions in. */
  85. private final String directory;
  86. /** Indicates whether or not user actions should be killed (not processed). */
  87. @ConfigBinding(domain = "actions", key = "killswitch")
  88. private boolean killSwitch;
  89. /**
  90. * Creates a new instance of ActionManager.
  91. *
  92. * @param serverManager The ServerManager in use.
  93. * @param identityManager The IdentityManager to load configuration from.
  94. * @param factory The factory to use to create new actions.
  95. * @param actionWrappersProvider Provider of action wrappers.
  96. * @param updateManagerProvider Provider of an update manager, to register components.
  97. * @param directory The directory to load and save actions in.
  98. */
  99. public ActionManager(
  100. final ServerManager serverManager,
  101. final IdentityController identityManager,
  102. final ActionFactory factory,
  103. final Provider<Set<ActionGroup>> actionWrappersProvider,
  104. final Provider<UpdateManager> updateManagerProvider,
  105. final String directory) {
  106. this.serverManager = serverManager;
  107. this.identityManager = identityManager;
  108. this.factory = factory;
  109. this.actionWrappersProvider = actionWrappersProvider;
  110. this.updateManagerProvider = updateManagerProvider;
  111. this.directory = directory;
  112. }
  113. /**
  114. * Get the server manager.
  115. *
  116. * @return ServerManager
  117. */
  118. public ServerManager getServerManager() {
  119. return serverManager;
  120. }
  121. /**
  122. * Create the singleton instance of the Action Manager.
  123. *
  124. * @param actionManager The manager to return for calls to {@link #getActionManager()}.
  125. *
  126. * @deprecated Singleton use should be removed.
  127. */
  128. @Deprecated
  129. public static void setActionManager(final ActionManager actionManager) {
  130. me = actionManager;
  131. }
  132. /**
  133. * Returns a singleton instance of the Action Manager.
  134. *
  135. * @return A singleton ActionManager instance
  136. */
  137. public static ActionManager getActionManager() {
  138. return me;
  139. }
  140. /**
  141. * Initialiases the actions manager.
  142. *
  143. * @param colourComparisons The colour comparisons to use.
  144. */
  145. // TODO: Refactor to take a list of comparisons/sources.
  146. public void initialise(final ColourActionComparison colourComparisons) {
  147. LOG.info("Initialising the actions manager");
  148. identityManager.getGlobalConfiguration().getBinder().bind(this, ActionManager.class);
  149. registerTypes(CoreActionType.values());
  150. registerComparisons(CoreActionComparison.values());
  151. registerComparisons(colourComparisons.getComparisons());
  152. registerComponents(CoreActionComponent.values());
  153. for (ActionGroup wrapper : actionWrappersProvider.get()) {
  154. addGroup(wrapper);
  155. }
  156. new WhoisNumericFormatter(identityManager.getAddonSettings()).register();
  157. // Register a listener for the closing event, so we can save actions
  158. registerListener(new ActionListener() {
  159. /** {@inheritDoc} */
  160. @Override
  161. public void processEvent(final ActionType type, final StringBuffer format,
  162. final Object... arguments) {
  163. saveAllActions();
  164. }
  165. }, CoreActionType.CLIENT_CLOSED);
  166. }
  167. /** {@inheritDoc} */
  168. @Override
  169. public void saveAllActions() {
  170. for (ActionGroup group : groups.values()) {
  171. for (Action action : group) {
  172. action.save();
  173. }
  174. }
  175. }
  176. /** {@inheritDoc} */
  177. @Override
  178. public void registerSetting(final String name, final String value) {
  179. LOG.debug("Registering new action setting: {} = {}", name, value);
  180. identityManager.getAddonSettings().setOption("actions", name, value);
  181. }
  182. /** {@inheritDoc} */
  183. @Override
  184. public void addGroup(final ActionGroup group) {
  185. groups.put(group.getName(), group);
  186. }
  187. /** {@inheritDoc} */
  188. @Override
  189. public void registerTypes(final ActionType[] newTypes) {
  190. for (ActionType type : newTypes) {
  191. checkNotNull(type);
  192. if (!types.contains(type)) {
  193. LOG.debug("Registering action type: {}", type);
  194. types.add(type);
  195. typeGroups.add(type.getType().getGroup(), type);
  196. }
  197. }
  198. }
  199. /** {@inheritDoc} */
  200. @Override
  201. public void registerComponents(final ActionComponent[] comps) {
  202. for (ActionComponent comp : comps) {
  203. checkNotNull(comp);
  204. LOG.debug("Registering action component: {}", comp);
  205. components.add(comp);
  206. }
  207. }
  208. /** {@inheritDoc} */
  209. @Override
  210. public void registerComparisons(final ActionComparison[] comps) {
  211. for (ActionComparison comp : comps) {
  212. checkNotNull(comp);
  213. LOG.debug("Registering action comparison: {}", comp);
  214. comparisons.add(comp);
  215. }
  216. }
  217. /** {@inheritDoc} */
  218. @Override
  219. public Map<String, ActionGroup> getGroupsMap() {
  220. return Collections.unmodifiableMap(groups);
  221. }
  222. /** {@inheritDoc} */
  223. @Override
  224. public MapList<String, ActionType> getGroupedTypes() {
  225. return new MapList<>(typeGroups);
  226. }
  227. /** {@inheritDoc} */
  228. @Override
  229. public void loadUserActions() {
  230. actions.clear();
  231. for (ActionGroup group : groups.values()) {
  232. group.clear();
  233. }
  234. final File dir = new File(directory);
  235. if (!dir.exists()) {
  236. try {
  237. dir.mkdirs();
  238. dir.createNewFile();
  239. } catch (IOException ex) {
  240. Logger.userError(ErrorLevel.HIGH, "I/O error when creating actions directory: "
  241. + ex.getMessage());
  242. }
  243. }
  244. if (dir.listFiles() == null) {
  245. Logger.userError(ErrorLevel.MEDIUM, "Unable to load user action files");
  246. } else {
  247. for (File file : dir.listFiles()) {
  248. if (file.isDirectory()) {
  249. loadActions(file);
  250. }
  251. }
  252. }
  253. registerComponents(updateManagerProvider.get());
  254. }
  255. /**
  256. * Creates new ActionGroupComponents for each action group.
  257. *
  258. * @param updateManager The update manager to register components with
  259. */
  260. private void registerComponents(final UpdateManager updateManager) {
  261. for (ActionGroup group : groups.values()) {
  262. if (group.getComponent() != -1 && group.getVersion() != null) {
  263. updateManager.addComponent(new ActionGroupComponent(group));
  264. }
  265. }
  266. }
  267. /**
  268. * Loads action files from a specified group directory.
  269. *
  270. * @param dir The directory to scan.
  271. */
  272. @Precondition("The specified File is not null and represents a directory")
  273. private void loadActions(final File dir) {
  274. checkNotNull(dir);
  275. checkArgument(dir.isDirectory());
  276. LOG.debug("Loading actions from directory: {}", dir.getAbsolutePath());
  277. if (!groups.containsKey(dir.getName())) {
  278. groups.put(dir.getName(), new ActionGroup(dir.getName()));
  279. }
  280. for (File file : dir.listFiles()) {
  281. factory.getAction(dir.getName(), file.getName());
  282. }
  283. }
  284. /** {@inheritDoc} */
  285. @Override
  286. public void addAction(final Action action) {
  287. checkNotNull(action);
  288. LOG.debug("Registering action: {}/{} (status: {})",
  289. new Object[]{action.getGroup(), action.getName(), action.getStatus()});
  290. if (action.getStatus() != ActionStatus.FAILED) {
  291. for (ActionType trigger : action.getTriggers()) {
  292. LOG.trace("Action has trigger {}", trigger);
  293. actions.add(trigger, action);
  294. }
  295. }
  296. getOrCreateGroup(action.getGroup()).add(action);
  297. }
  298. /** {@inheritDoc} */
  299. @Override
  300. public ActionGroup getOrCreateGroup(final String name) {
  301. if (!groups.containsKey(name)) {
  302. groups.put(name, new ActionGroup(name));
  303. }
  304. return groups.get(name);
  305. }
  306. /** {@inheritDoc} */
  307. @Override
  308. public void removeAction(final Action action) {
  309. checkNotNull(action);
  310. actions.removeFromAll(action);
  311. getOrCreateGroup(action.getGroup()).remove(action);
  312. }
  313. /** {@inheritDoc} */
  314. @Override
  315. public void reregisterAction(final Action action) {
  316. removeAction(action);
  317. addAction(action);
  318. }
  319. /** {@inheritDoc} */
  320. @Override
  321. public boolean triggerEvent(final ActionType type,
  322. final StringBuffer format, final Object... arguments) {
  323. checkNotNull(type);
  324. checkNotNull(type.getType());
  325. checkArgument(type.getType().getArity() == arguments.length);
  326. LOG.trace("Calling listeners for event of type {}", type);
  327. boolean res = false;
  328. if (listeners.containsKey(type)) {
  329. for (ActionListener listener : new ArrayList<>(listeners.get(type))) {
  330. try {
  331. listener.processEvent(type, format, arguments);
  332. } catch (Exception e) {
  333. Logger.appError(ErrorLevel.MEDIUM, "Error processing action: "
  334. + e.getMessage(), e);
  335. }
  336. }
  337. }
  338. if (!killSwitch) {
  339. res |= triggerActions(type, format, arguments);
  340. }
  341. return !res;
  342. }
  343. /**
  344. * Triggers actions that respond to the specified type.
  345. *
  346. * @param type The type of the event to process
  347. * @param format The format of the message that's going to be displayed for the event.
  348. * Actions may change this format.
  349. * @param arguments The arguments for the event
  350. *
  351. * @return True if the event should be skipped, or false if it can continue
  352. */
  353. @Precondition("The specified ActionType is not null")
  354. private boolean triggerActions(final ActionType type,
  355. final StringBuffer format, final Object... arguments) {
  356. checkNotNull(type);
  357. boolean res = false;
  358. LOG.trace("Executing actions for event of type {}", type);
  359. if (actions.containsKey(type)) {
  360. for (Action action : new ArrayList<>(actions.get(type))) {
  361. try {
  362. if (action.getConcurrencyGroup() == null) {
  363. res |= action.trigger(getServerManager(), format, arguments);
  364. } else {
  365. synchronized (locks) {
  366. if (!locks.containsKey(action.getConcurrencyGroup())) {
  367. locks.put(action.getConcurrencyGroup(), new Object());
  368. }
  369. }
  370. synchronized (locks.get(action.getConcurrencyGroup())) {
  371. res |= action.trigger(getServerManager(), format, arguments);
  372. }
  373. }
  374. } catch (LinkageError | Exception e) {
  375. Logger.appError(ErrorLevel.MEDIUM, "Error processing action: "
  376. + e.getMessage(), e);
  377. }
  378. }
  379. }
  380. return res;
  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(directory + 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: " + directory + 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(directory + 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. *
  520. * @throws IOException If the zip cannot be extracted
  521. */
  522. public static void installActionPack(final String path) throws IOException {
  523. final ZipResourceManager ziprm = ZipResourceManager.getInstance(path);
  524. ziprm.extractResources("", getActionManager().directory);
  525. getActionManager().loadUserActions();
  526. new File(path).delete();
  527. }
  528. /** {@inheritDoc} */
  529. @Override
  530. public void registerListener(final ActionListener listener, final ActionType... types) {
  531. for (ActionType type : types) {
  532. listeners.add(type, listener);
  533. }
  534. }
  535. /** {@inheritDoc} */
  536. @Override
  537. public void unregisterListener(final ActionListener listener, final ActionType... types) {
  538. for (ActionType type : types) {
  539. listeners.remove(type, listener);
  540. }
  541. }
  542. /** {@inheritDoc} */
  543. @Override
  544. public void unregisterListener(final ActionListener listener) {
  545. listeners.removeFromAll(listener);
  546. }
  547. }