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.

CommandFlagHandler.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /*
  2. * Copyright (c) 2006-2017 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  5. * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
  6. * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
  7. * permit persons to whom the Software is furnished to do so, subject to the following conditions:
  8. *
  9. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
  10. * Software.
  11. *
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  13. * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
  14. * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  15. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. */
  17. package com.dmdirc.commandparser.commands.flags;
  18. import com.dmdirc.commandparser.CommandArguments;
  19. import com.dmdirc.events.CommandErrorEvent;
  20. import com.dmdirc.interfaces.WindowModel;
  21. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.HashMap;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.stream.Collectors;
  28. import javax.annotation.Nullable;
  29. /**
  30. * Utility class for commands which allow the user to specify shell-like flags (
  31. * <code>--foo</code>).
  32. *
  33. * @since 0.6.5
  34. */
  35. public class CommandFlagHandler {
  36. /** A map of all known flag names to their flag objects. */
  37. private final Map<String, CommandFlag> flags = new HashMap<>();
  38. /** A map of currently enabled flag names to their flag objects. */
  39. private final Map<String, CommandFlag> enabledFlags = new HashMap<>();
  40. /** A map of currently disabled flag names to their flag objects. */
  41. private final Map<String, CommandFlag> disabledFlags = new HashMap<>();
  42. /** A map of disabled flag names to the flag objects that caused them to be disabled. */
  43. private final Map<String, CommandFlag> disabledBy = new HashMap<>();
  44. /**
  45. * Creates a new command flag handler which will handle all of the specified flags.
  46. *
  47. * @param flags The flags that will be handled
  48. */
  49. public CommandFlagHandler(final CommandFlag... flags) {
  50. for (CommandFlag flag : flags) {
  51. this.flags.put(flag.getName(), flag);
  52. }
  53. }
  54. /**
  55. * Processes the specified arguments and parses out command flags. If the specified arguments
  56. * aren't valid given the flags belonging to this command flag handler, an error message is sent
  57. * to the origin and <code>null</code> is returned from this method.
  58. *
  59. * @param origin The container where the command was entered
  60. * @param arguments The arguments passed to the command
  61. *
  62. * @return A corresponding {@link CommandFlagResult} object, or null if some problem was
  63. * encountered.
  64. */
  65. @Nullable
  66. public CommandFlagResult process(final WindowModel origin,
  67. final CommandArguments arguments) {
  68. final Map<CommandFlag, Integer> results = parse(origin, arguments);
  69. return results == null ? null : new CommandFlagResult(arguments, results);
  70. }
  71. /**
  72. * Parses the specified arguments and returns the offsets of the arguments for each found
  73. * command flag.
  74. *
  75. * @param origin The container where the command was entered
  76. * @param arguments The arguments passed to the command
  77. *
  78. * @return A map of discovered command flags to the offset of the flag's first argument within
  79. * the <code>arguments</code> object. If an error occurs, null is returned.
  80. */
  81. @Nullable
  82. protected Map<CommandFlag, Integer> parse(final WindowModel origin,
  83. final CommandArguments arguments) {
  84. enabledFlags.clear();
  85. disabledBy.clear();
  86. disabledFlags.clear();
  87. for (CommandFlag flag : flags.values()) {
  88. (flag.isEnabled() ? enabledFlags : disabledFlags).put(flag.getName(), flag);
  89. }
  90. final Map<CommandFlag, Integer> results = new HashMap<>();
  91. final Collection<CommandFlag> delayedFlags = new ArrayList<>(flags.size());
  92. int offset;
  93. for (offset = 0; offset < arguments.getArguments().length; offset++) {
  94. final String arg = arguments.getArguments()[offset];
  95. final String name;
  96. if (arg.startsWith("--")
  97. && flags.containsKey(name = arg.substring(2).toLowerCase())) {
  98. final CommandFlag flag = flags.get(name);
  99. if (enabledFlags.containsKey(name)) {
  100. // It's enabled!
  101. handleEnable(flag);
  102. // Handle any immediate arguments
  103. if ((offset = readArguments(flag, arguments, offset + 1,
  104. flag.getImmediateArgs(), origin, results)) == -1) {
  105. return null;
  106. }
  107. // Handle delayed arguments (if any)
  108. if (flag.getDelayedArgs() > 0) {
  109. delayedFlags.add(flag);
  110. }
  111. } else if (disabledBy.containsKey(name)) {
  112. // Disabled by another flag
  113. sendError(origin, arguments.isSilent(),
  114. "Cannot use flag --" + name
  115. + " in conjunction with --" + disabledBy.get(name).getName());
  116. return null;
  117. } else {
  118. // Disabled because not yet enabled
  119. sendError(origin, arguments.isSilent(),
  120. "Cannot use flag --" + name
  121. + " without " + getEnablers(flag));
  122. return null;
  123. }
  124. } else {
  125. break;
  126. }
  127. }
  128. // Handle any stored delayed arguments
  129. for (CommandFlag flag : delayedFlags) {
  130. if ((offset = readArguments(flag, arguments, offset,
  131. flag.getDelayedArgs(), origin, results)) == -1) {
  132. return null;
  133. }
  134. offset++;
  135. }
  136. results.put(null, offset);
  137. return results;
  138. }
  139. /**
  140. * Reads the arguments for the specified flag.
  141. *
  142. * @param flag The flag that is being read
  143. * @param arguments The raw arguments for the command
  144. * @param offset The index that the first argument will be at
  145. * @param argCount The number of arguments that need to be read
  146. * @param origin The source of the command (for error messages)
  147. * @param results The map to place results into
  148. *
  149. * @return The index of the last argument that was handled, or -1 if there were insufficient
  150. * arguments for the flag
  151. */
  152. protected int readArguments(final CommandFlag flag,
  153. final CommandArguments arguments, final int offset, final int argCount,
  154. final WindowModel origin, final Map<CommandFlag, Integer> results) {
  155. final int lastArg = argCount + offset - 1;
  156. if (arguments.getArguments().length <= lastArg) {
  157. sendError(origin, arguments.isSilent(),
  158. "Flag --" + flag.getName() + " expects "
  159. + argCount + " argument"
  160. + (argCount == 1 ? "" : "s"));
  161. return -1;
  162. }
  163. results.put(flag, offset);
  164. return lastArg;
  165. }
  166. /**
  167. * Processes the enabled and disabled lists for the specified flag, and adds them to the
  168. * relevant properties.
  169. *
  170. * @param flag The flag whose enables/disables lists should be processed
  171. */
  172. protected void handleEnable(final CommandFlag flag) {
  173. flag.getDisables().stream().filter(target -> enabledFlags.containsKey(target.getName()))
  174. .forEach(target -> {
  175. enabledFlags.remove(target.getName());
  176. disabledFlags.put(target.getName(), target);
  177. disabledBy.put(target.getName(), flag);
  178. });
  179. flag.getEnables().stream().filter(target -> disabledFlags.containsKey(target.getName()))
  180. .forEach(target -> {
  181. disabledFlags.remove(target.getName());
  182. enabledFlags.put(target.getName(), target);
  183. disabledBy.remove(target.getName());
  184. });
  185. }
  186. /**
  187. * Constructs a user-friendly string describing the flag(s) which must be used in order to
  188. * enable the specified flag. This is useful for error messages when the user tries to use a
  189. * disabled flag.
  190. *
  191. * @param flag The flag to find enablers for
  192. *
  193. * @return A user-friendly string describing flags which enable the specified flag.
  194. */
  195. protected String getEnablers(final CommandFlag flag) {
  196. final List<CommandFlag> enablers =
  197. flags.values().stream().filter(target -> target.getEnables().contains(flag))
  198. .collect(Collectors.toCollection(LinkedList::new));
  199. if (enablers.size() == 1) {
  200. return "--" + enablers.get(0).getName();
  201. }
  202. final StringBuilder res = new StringBuilder("one of ");
  203. for (CommandFlag enabler : enablers) {
  204. res.append("--");
  205. res.append(enabler.getName());
  206. res.append(", ");
  207. }
  208. return res.substring(0, res.length() - 2);
  209. }
  210. /**
  211. * Convenience method to send a command error event.
  212. *
  213. * @param origin The container to send the event to
  214. * @param isSilent Whether the command is silenced or not
  215. * @param message The error message
  216. */
  217. private static void sendError(final WindowModel origin, final boolean isSilent,
  218. final String message) {
  219. if (origin != null && !isSilent) {
  220. origin.getEventBus().publishAsync(new CommandErrorEvent(origin, message));
  221. }
  222. }
  223. }