/
ParametricCallable.java
471 lines (404 loc) · 17.7 KB
/
ParametricCallable.java
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.util.command.parametric;
import com.google.common.base.Joiner;
import com.sk89q.minecraft.util.commands.Command;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import com.sk89q.minecraft.util.commands.CommandLocals;
import com.sk89q.minecraft.util.commands.CommandPermissions;
import com.sk89q.minecraft.util.commands.CommandPermissionsException;
import com.sk89q.minecraft.util.commands.WrappedCommandException;
import com.sk89q.worldedit.util.command.CommandCallable;
import com.sk89q.worldedit.util.command.InvalidUsageException;
import com.sk89q.worldedit.util.command.MissingParameterException;
import com.sk89q.worldedit.util.command.Parameter;
import com.sk89q.worldedit.util.command.SimpleDescription;
import com.sk89q.worldedit.util.command.UnconsumedParameterException;
import com.sk89q.worldedit.util.command.binding.Switch;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The implementation of a {@link CommandCallable} for the {@link ParametricBuilder}.
*/
class ParametricCallable implements CommandCallable {
private final ParametricBuilder builder;
private final Object object;
private final Method method;
private final ParameterData[] parameters;
private final Set<Character> valueFlags = new HashSet<Character>();
private final SimpleDescription description = new SimpleDescription();
private final CommandPermissions commandPermissions;
/**
* Create a new instance.
*
* @param builder the parametric builder
* @param object the object to invoke on
* @param method the method to invoke
* @param definition the command definition annotation
* @throws ParametricException thrown on an error
*/
ParametricCallable(ParametricBuilder builder, Object object, Method method, Command definition) throws ParametricException {
this.builder = builder;
this.object = object;
this.method = method;
Annotation[][] annotations = method.getParameterAnnotations();
String[] names = builder.getParanamer().lookupParameterNames(method, false);
Type[] types = method.getGenericParameterTypes();
parameters = new ParameterData[types.length];
List<Parameter> userParameters = new ArrayList<Parameter>();
// This helps keep tracks of @Nullables that appear in the middle of a list
// of parameters
int numOptional = 0;
// Set permission hint
CommandPermissions permHint = method.getAnnotation(CommandPermissions.class);
if (permHint != null) {
description.setPermissions(Arrays.asList(permHint.value()));
}
// Go through each parameter
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ParameterData parameter = new ParameterData();
parameter.setType(type);
parameter.setModifiers(annotations[i]);
// Search for annotations
for (Annotation annotation : annotations[i]) {
if (annotation instanceof Switch) {
parameter.setFlag(((Switch) annotation).value(), type != boolean.class);
} else if (annotation instanceof Optional) {
parameter.setOptional(true);
String[] value = ((Optional) annotation).value();
if (value.length > 0) {
parameter.setDefaultValue(value);
}
// Special annotation bindings
} else if (parameter.getBinding() == null) {
parameter.setBinding(builder.getBindings().get(
annotation.annotationType()));
parameter.setClassifier(annotation);
}
}
parameter.setName(names.length > 0 ? names[i] : generateName(type, parameter.getClassifier(), i));
// Track all value flags
if (parameter.isValueFlag()) {
valueFlags.add(parameter.getFlag());
}
// No special @annotation binding... let's check for the type
if (parameter.getBinding() == null) {
parameter.setBinding(builder.getBindings().get(type));
// Don't know how to parse for this type of value
if (parameter.getBinding() == null) {
throw new ParametricException("Don't know how to handle the parameter type '" + type + "' in\n" + method.toGenericString());
}
}
// Do some validation of this parameter
parameter.validate(method, i + 1);
// Keep track of optional parameters
if (parameter.isOptional() && parameter.getFlag() == null) {
numOptional++;
} else {
if (numOptional > 0 && parameter.isNonFlagConsumer()) {
if (parameter.getConsumedCount() < 0) {
throw new ParametricException(
"Found an parameter using the binding " +
parameter.getBinding().getClass().getCanonicalName() +
"\nthat does not know how many arguments it consumes, but " +
"it follows an optional parameter\nMethod: " +
method.toGenericString());
}
}
}
parameters[i] = parameter;
// Make a list of "real" parameters
if (parameter.isUserInput()) {
userParameters.add(parameter);
}
}
// Finish description
description.setDescription(!definition.desc().isEmpty() ? definition.desc() : null);
description.setHelp(!definition.help().isEmpty() ? definition.help() : null);
description.overrideUsage(!definition.usage().isEmpty() ? definition.usage() : null);
for (InvokeListener listener : builder.getInvokeListeners()) {
listener.updateDescription(object, method, parameters, description);
}
// Set parameters
description.setParameters(userParameters);
// Get permissions annotation
commandPermissions = method.getAnnotation(CommandPermissions.class);
}
@Override
public boolean call(String stringArguments, CommandLocals locals, String[] parentCommands) throws CommandException {
// Test permission
if (!testPermission(locals)) {
throw new CommandPermissionsException();
}
String calledCommand = parentCommands.length > 0 ? parentCommands[parentCommands.length - 1] : "_";
String[] split = CommandContext.split(calledCommand + " " + stringArguments);
CommandContext context = new CommandContext(split, getValueFlags(), false, locals);
// Provide help if -? is specified
if (context.hasFlag('?')) {
String title = Joiner.on(" ").join(parentCommands);
throw new InvalidUsageException(null, this, true);
}
Object[] args = new Object[parameters.length];
ContextArgumentStack arguments = new ContextArgumentStack(context);
ParameterData parameter = null;
try {
// preProcess handlers
List<InvokeHandler> handlers = new ArrayList<InvokeHandler>();
for (InvokeListener listener : builder.getInvokeListeners()) {
InvokeHandler handler = listener.createInvokeHandler();
handlers.add(handler);
handler.preProcess(object, method, parameters, context);
}
// Collect parameters
for (int i = 0; i < parameters.length; i++) {
parameter = parameters[i];
if (mayConsumeArguments(i, arguments)) {
// Parse the user input into a method argument
ArgumentStack usedArguments = getScopedContext(parameter, arguments);
try {
args[i] = parameter.getBinding().bind(parameter, usedArguments, false);
} catch (MissingParameterException e) {
// Not optional? Then we can't execute this command
if (!parameter.isOptional()) {
throw e;
}
args[i] = getDefaultValue(i, arguments);
}
} else {
args[i] = getDefaultValue(i, arguments);
}
}
// Check for unused arguments
checkUnconsumed(arguments);
// preInvoke handlers
for (InvokeHandler handler : handlers) {
handler.preInvoke(object, method, parameters, args, context);
}
// Execute!
method.invoke(object, args);
// postInvoke handlers
for (InvokeHandler handler : handlers) {
handler.postInvoke(handler, method, parameters, args, context);
}
} catch (MissingParameterException e) {
throw new InvalidUsageException("Too few parameters!", this);
} catch (UnconsumedParameterException e) {
throw new InvalidUsageException("Too many parameters! Unused parameters: " + e.getUnconsumed(), this);
} catch (ParameterException e) {
assert parameter != null;
String name = parameter.getName();
throw new InvalidUsageException("For parameter '" + name + "': " + e.getMessage(), this);
} catch (InvocationTargetException e) {
for (ExceptionConverter converter : builder.getExceptionConverters()) {
converter.convert(e.getCause());
}
throw new WrappedCommandException(e);
} catch (IllegalArgumentException e) {
throw new WrappedCommandException(e);
} catch (CommandException e) {
throw e;
} catch (Throwable e) {
throw new WrappedCommandException(e);
}
return true;
}
@Override
public List<String> getSuggestions(String stringArguments, CommandLocals locals) throws CommandException {
return Collections.emptyList();
}
@Override
public Set<Character> getValueFlags() {
return valueFlags;
}
@Override
public SimpleDescription getDescription() {
return description;
}
@Override
public boolean testPermission(CommandLocals locals) {
if (commandPermissions != null) {
for (String perm : commandPermissions.value()) {
if (builder.getAuthorizer().testPermission(locals, perm)) {
return true;
}
}
return false;
} else {
return true;
}
}
/**
* Get the right {@link ArgumentStack}.
*
* @param parameter the parameter
* @param existing the existing scoped context
* @return the context to use
*/
private static ArgumentStack getScopedContext(Parameter parameter, ArgumentStack existing) {
if (parameter.getFlag() != null) {
CommandContext context = existing.getContext();
if (parameter.isValueFlag()) {
return new StringArgumentStack(
context, context.getFlag(parameter.getFlag()), false);
} else {
String v = context.hasFlag(parameter.getFlag()) ? "true" : "false";
return new StringArgumentStack(context, v, true);
}
}
return existing;
}
/**
* Get whether a parameter is allowed to consume arguments.
*
* @param i the index of the parameter
* @param scoped the scoped context
* @return true if arguments may be consumed
*/
private boolean mayConsumeArguments(int i, ContextArgumentStack scoped) {
CommandContext context = scoped.getContext();
ParameterData parameter = parameters[i];
// Flag parameters: Always consume
// Required non-flag parameters: Always consume
// Optional non-flag parameters:
// - Before required parameters: Consume if there are 'left over' args
// - At the end: Always consumes
if (parameter.isOptional() && parameter.getFlag() == null) {
int numberFree = context.argsLength() - scoped.position();
for (int j = i; j < parameters.length; j++) {
if (parameters[j].isNonFlagConsumer() && !parameters[j].isOptional()) {
// We already checked if the consumed count was > -1
// when we created this object
numberFree -= parameters[j].getConsumedCount();
}
}
// Skip this optional parameter
if (numberFree < 1) {
return false;
}
}
return true;
}
/**
* Get the default value for a parameter.
*
* @param i the index of the parameter
* @param scoped the scoped context
* @return a value
* @throws ParameterException on an error
* @throws CommandException on an error
*/
private Object getDefaultValue(int i, ContextArgumentStack scoped)
throws ParameterException, CommandException, InvocationTargetException {
CommandContext context = scoped.getContext();
ParameterData parameter = parameters[i];
String[] defaultValue = parameter.getDefaultValue();
if (defaultValue != null) {
try {
return parameter.getBinding().bind(
parameter, new StringArgumentStack(
context, defaultValue, false), false);
} catch (MissingParameterException e) {
throw new ParametricException(
"The default value of the parameter using the binding " +
parameter.getBinding().getClass() + " in the method\n" +
method.toGenericString() + "\nis invalid");
}
}
return null;
}
/**
* Check to see if all arguments, including flag arguments, were consumed.
*
* @param scoped the argument scope
* @throws UnconsumedParameterException thrown if parameters were not consumed
*/
private void checkUnconsumed(ContextArgumentStack scoped)
throws UnconsumedParameterException {
CommandContext context = scoped.getContext();
String unconsumed;
String unconsumedFlags = getUnusedFlags(context);
if ((unconsumed = scoped.getUnconsumed()) != null) {
throw new UnconsumedParameterException(unconsumed + " " + unconsumedFlags);
}
if (unconsumedFlags != null) {
throw new UnconsumedParameterException(unconsumedFlags);
}
}
/**
* Get any unused flag arguments.
*
* @param context the command context
*/
private String getUnusedFlags(CommandContext context) {
Set<Character> unusedFlags = null;
for (char flag : context.getFlags()) {
boolean found = false;
for (ParameterData parameter : parameters) {
Character paramFlag = parameter.getFlag();
if (paramFlag != null && flag == paramFlag) {
found = true;
break;
}
}
if (!found) {
if (unusedFlags == null) {
unusedFlags = new HashSet<Character>();
}
unusedFlags.add(flag);
}
}
if (unusedFlags != null) {
StringBuilder builder = new StringBuilder();
for (Character flag : unusedFlags) {
builder.append("-").append(flag).append(" ");
}
return builder.toString().trim();
}
return null;
}
/**
* Generate a name for a parameter.
*
* @param type the type
* @param classifier the classifier
* @param index the index
* @return a generated name
*/
private static String generateName(Type type, Annotation classifier, int index) {
if (classifier != null) {
return classifier.annotationType().getSimpleName().toLowerCase();
} else {
if (type instanceof Class<?>) {
return ((Class<?>) type).getSimpleName().toLowerCase();
} else {
return "unknown" + index;
}
}
}
}