Skip to content

Commit f7ce65d

Browse files
committed
Allow @mixin to be used for methods as well
1 parent 7b6ea15 commit f7ce65d

File tree

3 files changed

+288
-8
lines changed

3 files changed

+288
-8
lines changed

src/main/java/picocli/CommandLine.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2847,7 +2847,7 @@ private static class NoCompletionCandidates implements Iterable<String> {
28472847
* @since 3.0
28482848
*/
28492849
@Retention(RetentionPolicy.RUNTIME)
2850-
@Target(ElementType.FIELD)
2850+
@Target({ElementType.FIELD, ElementType.PARAMETER})
28512851
public @interface Mixin {
28522852
/** Optionally specify a name that the mixin object can be retrieved with from the {@code CommandSpec}.
28532853
* If not specified the name of the annotated field is used.
@@ -3718,15 +3718,28 @@ private void initResourceBundle(ResourceBundle bundle) {
37183718
* (class methods annotated with {@code @Command}) as subcommands.
37193719
*
37203720
* @return this {@link CommandSpec} object for method chaining
3721+
* @see #addMethodSubcommands(IFactory)
37213722
* @see #addSubcommand(String, CommandLine)
37223723
* @since 3.6.0
37233724
*/
37243725
public CommandSpec addMethodSubcommands() {
3726+
addMethodSubcommands(new DefaultFactory());
3727+
return this;
3728+
}
3729+
3730+
/** Reflects on the class of the {@linkplain #userObject() user object} and registers any command methods
3731+
* (class methods annotated with {@code @Command}) as subcommands.
3732+
*
3733+
* @return this {@link CommandSpec} object for method chaining
3734+
* @see #addSubcommand(String, CommandLine)
3735+
* @since 3.7.0
3736+
*/
3737+
public CommandSpec addMethodSubcommands(IFactory factory) {
37253738
if (userObject() instanceof Method) {
37263739
throw new UnsupportedOperationException("cannot discover methods of non-class: " + userObject());
37273740
}
37283741
for (Method method : getCommandMethods(userObject().getClass(), null)) {
3729-
CommandLine cmd = new CommandLine(method);
3742+
CommandLine cmd = new CommandLine(method, factory);
37303743
addSubcommand(cmd.getCommandName(), cmd);
37313744
}
37323745
return this;
@@ -3852,9 +3865,41 @@ public CommandSpec addMixin(String name, CommandSpec mixin) {
38523865
* @return an immutable list of all options and positional parameters for this command. */
38533866
public List<ArgSpec> args() { return Collections.unmodifiableList(args); }
38543867
Object[] argValues() {
3855-
int shift = mixinStandardHelpOptions() ? 2 : 0;
3856-
Object[] values = new Object[args.size() - shift];
3857-
for (int i = 0; i < values.length; i++) { values[i] = args.get(i + shift).getValue(); }
3868+
Map<Class<?>, CommandSpec> allMixins = null;
3869+
int argsLength = args.size();
3870+
int shift = 0;
3871+
for (Map.Entry<String, CommandSpec> mixinEntry : mixins.entrySet()) {
3872+
if (mixinEntry.getKey().equals(AutoHelpMixin.KEY)) {
3873+
shift = 2;
3874+
argsLength -= shift;
3875+
continue;
3876+
}
3877+
CommandSpec mixin = mixinEntry.getValue();
3878+
int mixinArgs = mixin.args.size();
3879+
argsLength -= (mixinArgs - 1); // sub 1 less b/c that's the mixin
3880+
if (allMixins == null) {
3881+
allMixins = new IdentityHashMap<Class<?>, CommandSpec>(mixins.size());
3882+
}
3883+
allMixins.put(mixin.userObject.getClass(), mixin);
3884+
}
3885+
3886+
Object[] values = new Object[argsLength];
3887+
if (allMixins == null) {
3888+
for (int i = 0; i < values.length; i++) { values[i] = args.get(i + shift).getValue(); }
3889+
} else {
3890+
int argIndex = shift;
3891+
Class<?>[] methodParams = ((Method) userObject).getParameterTypes();
3892+
for (int i = 0; i < methodParams.length; i++) {
3893+
final Class<?> param = methodParams[i];
3894+
CommandSpec mixin = allMixins.remove(param);
3895+
if (mixin == null) {
3896+
values[i] = args.get(argIndex++).getValue();
3897+
} else {
3898+
values[i] = mixin.userObject;
3899+
argIndex += mixin.args.size();
3900+
}
3901+
}
3902+
}
38583903
return values;
38593904
}
38603905

@@ -5640,7 +5685,7 @@ static boolean isAnnotated(AnnotatedElement e) {
56405685
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) { return accessible.isAnnotationPresent(annotationClass); }
56415686
<T extends Annotation> T getAnnotation(Class<T> annotationClass) { return accessible.getAnnotation(annotationClass); }
56425687
String name() { return name; }
5643-
boolean isArgSpec() { return isOption() || isParameter() || isMethodParameter(); }
5688+
boolean isArgSpec() { return isOption() || isParameter() || (isMethodParameter() && !isMixin()); }
56445689
boolean isOption() { return isAnnotationPresent(Option.class); }
56455690
boolean isParameter() { return isAnnotationPresent(Parameters.class); }
56465691
boolean isMixin() { return isAnnotationPresent(Mixin.class); }
@@ -5903,7 +5948,7 @@ private static void initSubcommands(Command cmd, CommandSpec parent, IFactory fa
59035948
}
59045949
}
59055950
if (cmd.addMethodSubcommands() && !(parent.userObject() instanceof Method)) {
5906-
parent.addMethodSubcommands();
5951+
parent.addMethodSubcommands(factory);
59075952
}
59085953
}
59095954
static void initParentCommand(Object subcommand, Object parent) {
@@ -5971,7 +6016,7 @@ private static boolean initFromMethodParameters(Object scope, Method method, Com
59716016
int optionCount = 0;
59726017
for (int i = 0, count = method.getParameterTypes().length; i < count; i++) {
59736018
MethodParam param = new MethodParam(method, i);
5974-
if (param.isAnnotationPresent(Option.class)) {
6019+
if (param.isAnnotationPresent(Option.class) || param.isAnnotationPresent(Mixin.class)) {
59756020
optionCount++;
59766021
} else {
59776022
param.position = i - optionCount;

src/test/java/picocli/CommandLineCommandMethodTest.java

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.junit.contrib.java.lang.system.ProvideSystemProperty;
2323
import org.junit.contrib.java.lang.system.SystemErrRule;
2424
import org.junit.contrib.java.lang.system.SystemOutRule;
25+
import picocli.CommandLine.Mixin;
2526
import picocli.CommandLineTest.CompactFields;
2627

2728
import java.io.ByteArrayOutputStream;
@@ -166,6 +167,226 @@ public void testAnnotateMethod_unannotatedPositionalMixedWithOptions_indexByPara
166167
assertEquals(String.class, spec.findOption("-c").type());
167168
}
168169

170+
@Command static class SomeMixin {
171+
@Option(names = "-a") int a;
172+
@Option(names = "-b") long b;
173+
}
174+
175+
static class UnannotatedClassWithMixinParameters {
176+
@Command
177+
void withMixin(@Mixin SomeMixin mixin) {
178+
}
179+
180+
@Command
181+
void posAndMixin(int[] x, @Mixin SomeMixin mixin) {
182+
}
183+
184+
@Command
185+
void posAndOptAndMixin(int[] x, @Option(names = "-y") String[] y, @Mixin SomeMixin mixin) {
186+
}
187+
188+
@Command
189+
void mixinFirst(@Mixin SomeMixin mixin, int[] x, @Option(names = "-y") String[] y) {
190+
}
191+
}
192+
193+
@Test
194+
public void testAnnotateMethod_mixinParameter() {
195+
Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "withMixin").get(0);
196+
CommandLine cmd = new CommandLine(m);
197+
Model.CommandSpec spec = cmd.getCommandSpec();
198+
assertEquals(1, spec.mixins().size());
199+
spec = spec.mixins().get("arg0");
200+
assertEquals(SomeMixin.class, spec.userObject().getClass());
201+
}
202+
203+
@Test
204+
public void testAnnotateMethod_positionalAndMixinParameter() {
205+
Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "posAndMixin").get(0);
206+
CommandLine cmd = new CommandLine(m);
207+
Model.CommandSpec spec = cmd.getCommandSpec();
208+
assertEquals(1, spec.mixins().size());
209+
assertEquals(1, spec.positionalParameters().size());
210+
spec = spec.mixins().get("arg1");
211+
assertEquals(SomeMixin.class, spec.userObject().getClass());
212+
}
213+
214+
@Test
215+
public void testAnnotateMethod_positionalAndOptionsAndMixinParameter() {
216+
Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "posAndOptAndMixin").get(0);
217+
CommandLine cmd = new CommandLine(m);
218+
Model.CommandSpec spec = cmd.getCommandSpec();
219+
assertEquals(1, spec.mixins().size());
220+
assertEquals(1, spec.positionalParameters().size());
221+
assertEquals(3, spec.options().size());
222+
spec = spec.mixins().get("arg2");
223+
assertEquals(SomeMixin.class, spec.userObject().getClass());
224+
}
225+
226+
@Test
227+
public void testAnnotateMethod_mixinParameterFirst() {
228+
Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "mixinFirst").get(0);
229+
CommandLine cmd = new CommandLine(m);
230+
Model.CommandSpec spec = cmd.getCommandSpec();
231+
assertEquals(1, spec.mixins().size());
232+
assertEquals(1, spec.positionalParameters().size());
233+
assertEquals(3, spec.options().size());
234+
spec = spec.mixins().get("arg0");
235+
assertEquals(SomeMixin.class, spec.userObject().getClass());
236+
}
237+
238+
static class UnannotatedClassWithMixinAndOptionsAndPositionals {
239+
@Command(name="sum")
240+
long sum(@Option(names = "-y") String[] y, @Mixin SomeMixin subMixin, int[] x) {
241+
return y.length + subMixin.a + subMixin.b + x.length;
242+
}
243+
}
244+
245+
@Test
246+
public void testUnannotatedCommandWithMixin() throws Exception {
247+
Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinAndOptionsAndPositionals.class, "sum").get(0);
248+
CommandLine commandLine = new CommandLine(m);
249+
List<CommandLine> parsed = commandLine.parse("-y foo -y bar -a 7 -b 11 13 42".split(" "));
250+
assertEquals(1, parsed.size());
251+
252+
// get method args
253+
Object[] methodArgValues = parsed.get(0).getCommandSpec().argValues();
254+
assertNotNull(methodArgValues);
255+
256+
// verify args
257+
String[] arg0 = (String[]) methodArgValues[0];
258+
assertArrayEquals(new String[] {"foo", "bar"}, arg0);
259+
SomeMixin arg1 = (SomeMixin) methodArgValues[1];
260+
assertEquals(7, arg1.a);
261+
assertEquals(11, arg1.b);
262+
int[] arg2 = (int[]) methodArgValues[2];
263+
assertArrayEquals(new int[] {13, 42}, arg2);
264+
265+
// verify method is callable with args
266+
long result = (Long) m.invoke(new UnannotatedClassWithMixinAndOptionsAndPositionals(), methodArgValues);
267+
assertEquals(22, result);
268+
269+
// verify same result with result handler
270+
List<Object> results = new CommandLine.RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF);
271+
assertEquals(1, results.size());
272+
assertEquals(22L, results.get(0));
273+
}
274+
275+
@Command
276+
static class AnnotatedClassWithMixinParameters {
277+
@Mixin SomeMixin mixin;
278+
279+
@Command(name="sum")
280+
long sum(@Option(names = "-y") String[] y, @Mixin SomeMixin subMixin, int[] x) {
281+
return mixin.a + mixin.b + y.length + subMixin.a + subMixin.b + x.length;
282+
}
283+
}
284+
285+
@Test
286+
public void testAnnotatedSubcommandWithDoubleMixin() throws Exception {
287+
AnnotatedClassWithMixinParameters command = new AnnotatedClassWithMixinParameters();
288+
CommandLine commandLine = new CommandLine(command);
289+
List<CommandLine> parsed = commandLine.parse("-a 3 -b 5 sum -y foo -y bar -a 7 -b 11 13 42".split(" "));
290+
assertEquals(2, parsed.size());
291+
292+
// get method args
293+
Object[] methodArgValues = parsed.get(1).getCommandSpec().argValues();
294+
assertNotNull(methodArgValues);
295+
296+
// verify args
297+
String[] arg0 = (String[]) methodArgValues[0];
298+
assertArrayEquals(new String[] {"foo", "bar"}, arg0);
299+
SomeMixin arg1 = (SomeMixin) methodArgValues[1];
300+
assertEquals(7, arg1.a);
301+
assertEquals(11, arg1.b);
302+
int[] arg2 = (int[]) methodArgValues[2];
303+
assertArrayEquals(new int[] {13, 42}, arg2);
304+
305+
// verify method is callable with args
306+
Method m = AnnotatedClassWithMixinParameters.class.getDeclaredMethod("sum", String[].class, SomeMixin.class, int[].class);
307+
long result = (Long) m.invoke(command, methodArgValues);
308+
assertEquals(30, result);
309+
310+
// verify same result with result handler
311+
List<Object> results = new CommandLine.RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF);
312+
assertEquals(1, results.size());
313+
assertEquals(30L, results.get(0));
314+
}
315+
316+
@Command static class OtherMixin {
317+
@Option(names = "-c") int c;
318+
}
319+
320+
static class AnnotatedClassWithMultipleMixinParameters {
321+
@Command(name="sum")
322+
long sum(@Mixin SomeMixin mixin1, @Mixin OtherMixin mixin2) {
323+
return mixin1.a + mixin1.b + mixin2.c;
324+
}
325+
}
326+
327+
@Test
328+
public void testAnnotatedMethodMultipleMixinsSubcommandWithDoubleMixin() throws Exception {
329+
Method m = CommandLine.getCommandMethods(AnnotatedClassWithMultipleMixinParameters.class, "sum").get(0);
330+
CommandLine commandLine = new CommandLine(m);
331+
List<CommandLine> parsed = commandLine.parse("-a 3 -b 5 -c 7".split(" "));
332+
assertEquals(1, parsed.size());
333+
334+
// get method args
335+
Object[] methodArgValues = parsed.get(0).getCommandSpec().argValues();
336+
assertNotNull(methodArgValues);
337+
338+
// verify args
339+
SomeMixin arg0 = (SomeMixin) methodArgValues[0];
340+
assertEquals(3, arg0.a);
341+
assertEquals(5, arg0.b);
342+
OtherMixin arg1 = (OtherMixin) methodArgValues[1];
343+
assertEquals(7, arg1.c);
344+
345+
// verify method is callable with args
346+
long result = (Long) m.invoke(new AnnotatedClassWithMultipleMixinParameters(), methodArgValues);
347+
assertEquals(15, result);
348+
349+
// verify same result with result handler
350+
List<Object> results = new CommandLine.RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF);
351+
assertEquals(1, results.size());
352+
assertEquals(15L, results.get(0));
353+
}
354+
355+
@Command static class EmptyMixin {}
356+
357+
static class AnnotatedClassWithMultipleEmptyParameters {
358+
@Command(name="sum")
359+
long sum(@Option(names = "-a") int a, @Mixin EmptyMixin mixin) {
360+
return a;
361+
}
362+
}
363+
364+
@Test
365+
public void testAnnotatedMethodMultipleMixinsSubcommandWithEmptyMixin() throws Exception {
366+
Method m = CommandLine.getCommandMethods(AnnotatedClassWithMultipleEmptyParameters.class, "sum").get(0);
367+
CommandLine commandLine = new CommandLine(m);
368+
List<CommandLine> parsed = commandLine.parse("-a 3".split(" "));
369+
assertEquals(1, parsed.size());
370+
371+
// get method args
372+
Object[] methodArgValues = parsed.get(0).getCommandSpec().argValues();
373+
assertNotNull(methodArgValues);
374+
375+
// verify args
376+
int arg0 = (Integer) methodArgValues[0];
377+
assertEquals(3, arg0);
378+
EmptyMixin arg1 = (EmptyMixin) methodArgValues[1];
379+
380+
// verify method is callable with args
381+
long result = (Long) m.invoke(new AnnotatedClassWithMultipleEmptyParameters(), methodArgValues);
382+
assertEquals(3, result);
383+
384+
// verify same result with result handler
385+
List<Object> results = new CommandLine.RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF);
386+
assertEquals(1, results.size());
387+
assertEquals(3L, results.get(0));
388+
}
389+
169390
@Test
170391
public void testAnnotateMethod_annotated() throws Exception {
171392
Method m = CommandLine.getCommandMethods(MethodApp.class, "run2").get(0);

src/test/java/picocli/CommandLineMixinTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ class ValidMixin { // valid command because it has @Parameters annotation
9292
commandLine.addMixin("valid", new ValidMixin()); // no exception
9393
}
9494

95+
@Test
96+
public void testAddMixinMustBeValidCommand_SubCommandMethod() {
97+
@Command class ValidMixin { // valid command because it has @Command annotation
98+
}
99+
@Command class Receiver {
100+
@Command void sub(@Mixin ValidMixin mixin) {
101+
}
102+
}
103+
CommandLine commandLine = new CommandLine(new Receiver(), new InnerClassFactory(this));
104+
CommandSpec commandSpec = commandLine.getCommandSpec().subcommands().get("sub").getCommandSpec().mixins().get("arg0");
105+
assertEquals(ValidMixin.class, commandSpec.userObject().getClass());
106+
commandLine.addMixin("valid", new ValidMixin()); // no exception
107+
}
108+
95109
@Test
96110
public void testMixinAnnotationRejectedIfNotAValidCommand() {
97111
class Invalid {}

0 commit comments

Comments
 (0)