summaryrefslogtreecommitdiff
path: root/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java
blob: e527f2792a136c7fe795cfc0a7549f68e7959e1f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
package io.dico.dicore.command;

import io.dico.dicore.command.chat.IChatHandler;
import io.dico.dicore.command.parameter.type.IParameterTypeSelector;
import io.dico.dicore.command.parameter.type.MapBasedParameterTypeSelector;
import io.dico.dicore.command.parameter.type.ParameterType;
import io.dico.dicore.command.predef.HelpCommand;
import io.dico.dicore.command.predef.PredefinedCommand;
import io.dico.dicore.command.predef.SyntaxCommand;
import io.dico.dicore.command.registration.reflect.ReflectiveRegistration;

import java.util.HashSet;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * Mimic of WorldEdit's CommandGraph
 */
public final class CommandBuilder {
    private final RootCommandAddress root;
    private ModifiableCommandAddress cur;
    private IParameterTypeSelector selector;

    /**
     * Instantiate a new CommandBuilder with a new command root system
     * Commands registered to this command builder might interfere with
     * commands registered to other commands builders or by other plugins.
     */
    public CommandBuilder() {
        this(new RootCommandAddress());
    }

    /**
     * Instantiate a new CommandBuilder with a specified root address.
     * If the root address is identical to that of another command builder,
     * they will modify the same tree.
     *
     * @param root the root address
     */
    public CommandBuilder(RootCommandAddress root) {
        this.root = Objects.requireNonNull(root);
        this.cur = root;
        this.selector = new MapBasedParameterTypeSelector(true);
    }

    /**
     * Add a sub command at the current address
     * The current address can be inspected using {@link #getAddress()}
     *
     * @param name    the name of the command
     * @param command the command executor
     * @param aliases any aliases
     * @return this
     */
    public CommandBuilder addSubCommand(String name, Command command, String... aliases) {
        ChildCommandAddress address = new ChildCommandAddress(command);
        address.addNameAndAliases(name, aliases);
        return addSubCommand(address);
    }

    /**
     * Add a subcommand as an address at the current address
     * The result of this call is the same as
     * {@code addSubCommand(address.getMainKey(), address.getCommand(), address.getNames().sublist(1).toArray(new String[0]))}
     *
     * @param address the address
     * @return this
     * @throws IllegalArgumentException if {@code address.isRoot()}
     */
    public CommandBuilder addSubCommand(ICommandAddress address) {
        cur.addChild(address);
        return this;
    }

    /**
     * Search the given class for any (static) methods using command annotations
     * The class gets a localized parameter type selector if it defines parameter types.
     * Any commands found are registered as sub commands to the current address.
     *
     * @param clazz the clazz
     * @return this
     * @throws IllegalArgumentException if an exception occurs while parsing the methods of this class
     * @see #registerCommands(Class, Object)
     */
    public CommandBuilder registerCommands(Class<?> clazz) {
        return registerCommands(clazz, null);
    }

    /**
     * Search the given object's class for methods using command annotations.
     * If the object is null, only static methods are checked. Otherwise, instance methods are also checked.
     * The class gets a localized parameter type selector if it defines parameter types.
     * Any commands found are registered as sub commands to the current address.
     *
     * @param object the object
     * @return this
     * @throws IllegalArgumentException if an exception occurs while parsing the methods of this class
     * @see #registerCommands(Class, Object)
     */
    public CommandBuilder registerCommands(Object object) {
        return registerCommands(object.getClass(), object);
    }

    /**
     * Search the given class for methods using command annotations.
     * The class gets a localized parameter type selector if it defines parameter types.
     * Any commands found are registered as sub commands to the current address.
     * The instance is used to invoke non-static methods.
     *
     * @param clazz    the class
     * @param instance the instance, null if only static methods
     * @return this
     * @throws IllegalArgumentException if instance is not null and it's not an instance of the class
     * @throws IllegalArgumentException if another exception occurs while parsing the methods of this class
     */
    public CommandBuilder registerCommands(Class<?> clazz, Object instance) {
        try {
            ReflectiveRegistration.parseCommandGroup(cur, selector, clazz, instance);
            return this;
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    /**
     * register the {@link HelpCommand} as a sub command at the current address
     *
     * @return this
     */
    public CommandBuilder registerHelpCommand() {
        HelpCommand.registerAsChild(cur);
        return this;
    }

    /**
     * register the {@link SyntaxCommand} as a sub command a the current address
     *
     * @return this
     */
    public CommandBuilder registerSyntaxCommand() {
        SyntaxCommand.registerAsChild(cur);
        return this;
    }

    /**
     * Generate the predefined commands.
     * These are presets.
     * Examples include {@code help} and {@code syntax}.
     * <p>
     * Predefined commands can be registered through {@link PredefinedCommand#registerPredefinedCommandGenerator(String, Consumer)}
     *
     * @param commands the commands
     * @return this
     */
    public CommandBuilder generatePredefinedCommands(String... commands) {
        ReflectiveRegistration.generateCommands(cur, commands);
        return this;
    }

    /**
     * Unregister any childs present at the given keys.
     * <p>
     * This method can be used to remove unwanted keys, that might have been added
     * outside of your control. For example, because you didn't want all commands
     * registered by {@link #registerCommands(Class, Object)}, or because you didn't
     * want the help command registered by {@link #group(String, String...)}
     *
     * @param removeAliases true if any aliases of the children present at the keys should be removed
     * @param keys          a varargs array containing the keys
     * @return this
     * @throws IllegalArgumentException if keys array is empty
     */
    public CommandBuilder unregisterCommands(boolean removeAliases, String... keys) {
        cur.removeChildren(removeAliases, keys);
        return this;
    }

    /**
     * Jump to the sub-address with the given name as main key.
     * If an address with the exact name as main key exists,
     * that address becomes the current address.
     * <p>
     * Otherwise, a new addresses is registered with the name and aliases.
     * New addresses registered by this command have a HelpCommand added by default.
     * <p>
     * After this call, any registered commands are registered as a sub command
     * to the new address. To restore the previous state, a call to {@link #parent()}
     * should be made.
     * <p>
     * If the address is the target of a command, it will provide information about its sub commands
     * using the HelpCommand.
     *
     * @param name    the main key
     * @param aliases the aliases
     * @return this
     */
    public CommandBuilder group(String name, String... aliases) {
        ChildCommandAddress address = cur.getChild(name);
        if (address == null || !name.equals(address.getMainKey())) {
            address = new ChildCommandAddress();
            address.setupAsPlaceholder(name, aliases);
            cur.addChild(address);
        }
        cur = address;
        return this;
    }

    /**
     * Similar to {@link #group(String, String[])} but this will force overwrite any present group,
     * using the address passed. The address MUST be an instance of {@link ChildCommandAddress}.
     *
     * <p>The address must not have a parent or any keys</p>
     *
     * @param address the address object to use
     * @param name the main key
     * @param aliases any aliases
     * @return this
     * @throws IllegalArgumentException if any of the requirements set out above aren't met
     */
    public CommandBuilder group(ICommandAddress address, String name, String... aliases) {
        if (address.hasParent() || address.getMainKey() != null || !(address instanceof ChildCommandAddress)) {
            throw new IllegalArgumentException();
        }

        ChildCommandAddress asChild = (ChildCommandAddress) address;
        asChild.setupAsPlaceholder(name, aliases);
        cur.addChild(address);
        cur = asChild;
        return this;
    }

    /**
     * Sets the description of a group created by {@link #group(String, String...)}
     * Can be called subsequently to making a call to {@link #group(String, String...)}
     *
     * @param shortDescription a short description
     * @param description      the lines of a full description.
     * @return this
     * @throws IllegalStateException if the current group has no command
     */
    public CommandBuilder setGroupDescription(String shortDescription, String... description) {
        Command command = cur.getCommand();
        if (command == null) throw new IllegalStateException();
        cur.setCommand(command
                .setShortDescription(shortDescription)
                .setDescription(description));
        return this;
    }

    /**
     * Add a context filter to the command of the current group
     * @return this
     * @throws IllegalStateException if the current group has no command
     */
    public CommandBuilder addContextFilter(IContextFilter contextFilter) {
        Command command = cur.getCommand();
        if (command == null) throw new IllegalStateException();
        cur.setCommand(command
            .addContextFilter(contextFilter));
        return this;
    }

    /**
     * Add a required permission to the command of the current group
     * @return this
     * @throws IllegalStateException if the current group has no command
     */
    public CommandBuilder addPermission(String permission) {
        return addContextFilter(IContextFilter.permission(permission));
    }

    /**
     * Add a required permission to the command of the current group, which can be inherited
     * @return this
     * @throws IllegalStateException if the current group has no command
     */
    public CommandBuilder addInheritablePermission(String permission) {
        return addContextFilter(IContextFilter.inheritablePermission(permission));
    }

    /**
     * Jump up a level in the address
     *
     * @return this
     * @throws IllegalStateException if the address is empty
     *                               // has a depth of 0 // is at level 0
     */
    public CommandBuilder parent() {
        if (cur.hasParent()) {
            cur = cur.getParent();
            return this;
        }
        throw new IllegalStateException("No parent exists at this address");
    }

    /**
     * Jump to the root (empty) address,
     * such that a subsequent call to {@link #parent()}
     * will throw a {@link IllegalStateException}
     *
     * @return this
     */
    public CommandBuilder root() {
        cur = root;
        return this;
    }

    /**
     * Get the current address, as a space-separated string
     *
     * @return the current address
     */
    public String getAddress() {
        return cur.getAddress();
    }

    /**
     * Get the depth of the current address.
     * This is equivalent to {@code getAddress().split(" ").length}.
     * If the address is empty, the depth is 0.
     *
     * @return the depth
     */
    public int getDepth() {
        return cur.getDepth();
    }

    /**
     * Set the command at the current group. The command is set
     * a level higher than it would be if this were a call to {@link #addSubCommand(String, Command, String...)}
     * <p>
     * If a call to {@link #setGroupDescription(String, String...)} was made at the same address before,
     * the description is copied to the given executor.
     *
     * @param command the executor
     * @return this
     * @throws IllegalArgumentException if the command at the address is present and declared by the user,
     *                                  in other words, it's not a {@link PredefinedCommand}
     */
    public CommandBuilder setCommand(Command command) {
        Command current = cur.getCommand();
        if (current instanceof HelpCommand && current != HelpCommand.INSTANCE) {
            command.setShortDescription(current.getShortDescription());
            command.setDescription(current.getDescription());
        }

        cur.setCommand(command);
        return this;
    }

    /**
     * Configure the chat handler at this address. The chat handler
     * is used for all children down the tree if they don't explicitly have
     * their own chat handler configured. If this isn't configured,
     * {@code ChatHandlers.defaultChat()} is used.
     *
     * @param chatHandler the chat handler
     * @return this
     */
    public CommandBuilder setChatHandler(IChatHandler chatHandler) {
        cur.setChatHandler(chatHandler);
        return this;
    }

    /**
     * Add the parameter type to this builder's selector.
     *
     * @param type the type
     * @param <T>  the return type of the parameter type
     * @return this
     */
    public <T> CommandBuilder addParameterType(ParameterType<T, Void> type) {
        selector.addType(false, type);
        return this;
    }

    /**
     * Add the parameter type to this builder's selector.
     *
     * @param infolessAlias whether to also register the type with an infoless alias.
     *                      this increases the priority assigned to the type if no info object is present.
     * @param type          the type
     * @param <T>           the return type of the parameter type
     * @param <C>           the parameter config type (info object)
     * @return this
     */

    public <T, C> CommandBuilder addParameterType(boolean infolessAlias, ParameterType<T, C> type) {
        selector.addType(infolessAlias, type);
        return this;
    }

    /**
     * Get the dispatcher for the root address.
     * The dispatcher should be used to finally register all commands,
     * after they are all declared.
     *
     * @return the dispatcher
     */
    public ICommandDispatcher getDispatcher() {
        return root;
    }

    /**
     * Print debugging information about the current addresses and commands in this builder
     * A StackTraceElement indicating where this was called from is also included
     *
     * @return this
     */
    public CommandBuilder printDebugInformation() {
        String address = cur == root ? "<root>" : cur.getAddress();
        StackTraceElement caller = getCallsite();

        StringBuilder message = new StringBuilder("### CommandBuilder dump ###");
        message.append("\nCalled from ").append(caller);
        message.append("\nPosition: ").append(address);
        cur.appendDebugInformation(message, "", new HashSet<>());

        System.out.println(message);
        return this;
    }

    private static StackTraceElement getCallsite() {
        // [0] Thread.currentThread()
        // [1] CommandBuilder.getCallsite()
        // [2] Calling method
        // [3] Method calling the calling method
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        return trace.length > 3 ? trace[3] : null;
    }

}