Skip to content

Commit a37bd6a

Browse files
committed
Fix alias handling with @command annotation
- Revisit how alias commands are added using @command annotation when using if/or on class and/or method level. - With this change alias handling is more logical and there's better tests and docs. - Fixes #945
1 parent c030193 commit a37bd6a

File tree

8 files changed

+409
-11
lines changed

8 files changed

+409
-11
lines changed

spring-shell-core/src/main/java/org/springframework/shell/command/annotation/Command.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -69,8 +69,8 @@
6969
* {@code alias1 sub1} it can be defined as:
7070
*
7171
* <pre class="code">
72-
* command = { "alias1", "sub1" }
73-
* command = "alias1 sub1"
72+
* alias = { "alias1", "sub1" }
73+
* alias = "alias1 sub1"
7474
* </pre>
7575
*
7676
* Values are split and trimmed meaning spaces doesn't matter.

spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtils.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -159,10 +159,11 @@ private static String[][] deduceStringArrayLeftPrefixes(String field, MergedAnno
159159
.collect(Collectors.toList());
160160

161161
return Stream.of(right.getStringArray(field))
162-
.flatMap(command -> Stream.of(command.split(" ")))
163-
.filter(command -> StringUtils.hasText(command))
164162
.map(command -> command.strip())
165-
.map(command -> Stream.concat(prefix.stream(), Stream.of(command)).collect(Collectors.toList()))
163+
.map(command -> Stream.concat(
164+
prefix.stream(),
165+
Stream.of(command).filter(c -> StringUtils.hasText(c)))
166+
.collect(Collectors.toList()))
166167
.map(arr -> arr.toArray(String[]::new))
167168
.toArray(String[][]::new);
168169
}

spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtilsTests.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -108,6 +108,10 @@ void testCommand() {
108108
.get(Command.class);
109109
private static MergedAnnotation<Command> aliasValues4 = MergedAnnotations.from(AliasValues4.class)
110110
.get(Command.class);
111+
private static MergedAnnotation<Command> aliasValues5 = MergedAnnotations.from(AliasValues5.class)
112+
.get(Command.class);
113+
private static MergedAnnotation<Command> aliasValues6 = MergedAnnotations.from(AliasValues6.class)
114+
.get(Command.class);
111115

112116
@Command
113117
private static class AliasDefault {
@@ -129,6 +133,14 @@ private static class AliasValues3 {
129133
private static class AliasValues4 {
130134
}
131135

136+
@Command(alias = { "one" })
137+
private static class AliasValues5 {
138+
}
139+
140+
@Command(alias = { "" })
141+
private static class AliasValues6 {
142+
}
143+
132144
@Test
133145
void testAlias() {
134146
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasDefault)).isEmpty();
@@ -139,7 +151,9 @@ void testAlias() {
139151
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasValues3))
140152
.isEqualTo(new String[][] { { "five" }, { "six" }, { "seven" } });
141153
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasValues4))
142-
.isEqualTo(new String[][] { { "eight" }, { "nine" } });
154+
.isEqualTo(new String[][] { { "eight nine" } });
155+
assertThat(CommandAnnotationUtils.deduceAlias(aliasValues5, aliasValues6))
156+
.isEqualTo(new String[][] { { "one" } });
143157
}
144158

145159
private static MergedAnnotation<Command> groupValue1 = MergedAnnotations.from(GroupValues1.class)

spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717

1818
import java.util.Collections;
1919

20+
import org.junit.jupiter.api.Nested;
2021
import org.junit.jupiter.api.Test;
2122

2223
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -375,4 +376,186 @@ private <T> ApplicationContextRunner configCommon(Class<T> type, T bean, String
375376
bd.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_METHOD_PARAMETERS, parameters);
376377
});
377378
}
379+
380+
@Nested
381+
class Aliases {
382+
383+
@Test
384+
void aliasOnlyOnMethod() {
385+
configCommon(AliasOnlyOnMethod.class, new AliasOnlyOnMethod(), "command1", new Class[] { })
386+
.run((context) -> {
387+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
388+
CommandRegistrationFactoryBean.class);
389+
assertThat(fb).isNotNull();
390+
CommandRegistration registration = fb.getObject();
391+
assertThat(registration).isNotNull();
392+
assertThat(registration.getCommand()).isEqualTo("one two");
393+
assertThat(registration.getAliases()).hasSize(1);
394+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("four");
395+
});
396+
}
397+
398+
@Command(command = "one")
399+
private static class AliasOnlyOnMethod {
400+
401+
@Command(command = "two", alias = "four")
402+
void command1(){
403+
}
404+
}
405+
406+
@Test
407+
void aliasOnlyOnClass() {
408+
configCommon(AliasOnlyOnClass.class, new AliasOnlyOnClass(), "command1", new Class[] { })
409+
.run((context) -> {
410+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
411+
CommandRegistrationFactoryBean.class);
412+
assertThat(fb).isNotNull();
413+
CommandRegistration registration = fb.getObject();
414+
assertThat(registration).isNotNull();
415+
assertThat(registration.getCommand()).isEqualTo("one two");
416+
assertThat(registration.getAliases()).hasSize(0);
417+
});
418+
}
419+
420+
@Command(command = "one", alias = "three")
421+
private static class AliasOnlyOnClass {
422+
423+
@Command(command = "two")
424+
void command1(){
425+
}
426+
}
427+
428+
@Test
429+
void aliasOnlyOnMethodMultiCommandString() {
430+
configCommon(AliasOnlyOnMethodMultiCommandString.class, new AliasOnlyOnMethodMultiCommandString(), "command1", new Class[] { })
431+
.run((context) -> {
432+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
433+
CommandRegistrationFactoryBean.class);
434+
assertThat(fb).isNotNull();
435+
CommandRegistration registration = fb.getObject();
436+
assertThat(registration).isNotNull();
437+
assertThat(registration.getCommand()).isEqualTo("one two");
438+
assertThat(registration.getAliases()).hasSize(1);
439+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("four five");
440+
});
441+
}
442+
443+
@Command(command = "one")
444+
private static class AliasOnlyOnMethodMultiCommandString {
445+
446+
@Command(command = "two", alias = "four five")
447+
void command1(){
448+
}
449+
}
450+
451+
@Test
452+
void aliasOnlyOnMethodMultiCommandArray() {
453+
configCommon(AliasOnlyOnMethodMultiCommandArray.class, new AliasOnlyOnMethodMultiCommandArray(), "command1", new Class[] { })
454+
.run((context) -> {
455+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
456+
CommandRegistrationFactoryBean.class);
457+
assertThat(fb).isNotNull();
458+
CommandRegistration registration = fb.getObject();
459+
assertThat(registration).isNotNull();
460+
assertThat(registration.getCommand()).isEqualTo("one two");
461+
assertThat(registration.getAliases()).hasSize(2);
462+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("four");
463+
assertThat(registration.getAliases().get(1).getCommand()).isEqualTo("five");
464+
});
465+
}
466+
467+
@Command(command = "one")
468+
private static class AliasOnlyOnMethodMultiCommandArray {
469+
470+
@Command(command = "two", alias = {"four", "five"})
471+
void command1(){
472+
}
473+
}
474+
475+
@Test
476+
void aliasOnBothMethodStringEmpty() {
477+
configCommon(AliasOnBothMethodStringEmpty.class, new AliasOnBothMethodStringEmpty(), "command1", new Class[] { })
478+
.run((context) -> {
479+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
480+
CommandRegistrationFactoryBean.class);
481+
assertThat(fb).isNotNull();
482+
CommandRegistration registration = fb.getObject();
483+
assertThat(registration).isNotNull();
484+
assertThat(registration.getCommand()).isEqualTo("one two");
485+
assertThat(registration.getAliases()).hasSize(1);
486+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("three");
487+
});
488+
}
489+
490+
@Command(command = "one", alias = "three")
491+
private static class AliasOnBothMethodStringEmpty {
492+
493+
@Command(command = "two", alias = "")
494+
void command1(){
495+
}
496+
}
497+
498+
@Test
499+
void aliasOnBoth() {
500+
configCommon(AliasOnBoth.class, new AliasOnBoth(), "command1", new Class[] { })
501+
.run((context) -> {
502+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
503+
CommandRegistrationFactoryBean.class);
504+
assertThat(fb).isNotNull();
505+
CommandRegistration registration = fb.getObject();
506+
assertThat(registration).isNotNull();
507+
assertThat(registration.getCommand()).isEqualTo("one two");
508+
assertThat(registration.getAliases()).hasSize(1);
509+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("three four");
510+
});
511+
}
512+
513+
@Command(command = "one", alias = "three")
514+
private static class AliasOnBoth {
515+
516+
@Command(command = "two", alias = "four")
517+
void command1(){
518+
}
519+
}
520+
521+
@Test
522+
void aliasWithCommandOnBothMethodStringEmpty() {
523+
configCommon(AliasWithCommandOnBothMethodStringEmpty.class, new AliasWithCommandOnBothMethodStringEmpty(), "command1", new Class[] { })
524+
.run((context) -> {
525+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
526+
CommandRegistrationFactoryBean.class);
527+
assertThat(fb).isNotNull();
528+
CommandRegistration registration = fb.getObject();
529+
assertThat(registration).isNotNull();
530+
assertThat(registration.getCommand()).isEqualTo("one");
531+
assertThat(registration.getAliases()).hasSize(1);
532+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("ten");
533+
});
534+
configCommon(AliasWithCommandOnBothMethodStringEmpty.class, new AliasWithCommandOnBothMethodStringEmpty(), "command2", new Class[] { })
535+
.run((context) -> {
536+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
537+
CommandRegistrationFactoryBean.class);
538+
assertThat(fb).isNotNull();
539+
CommandRegistration registration = fb.getObject();
540+
assertThat(registration).isNotNull();
541+
assertThat(registration.getCommand()).isEqualTo("one two");
542+
assertThat(registration.getAliases()).hasSize(1);
543+
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("ten twelve");
544+
});
545+
}
546+
547+
@Command(command = "one", alias = "ten")
548+
private static class AliasWithCommandOnBothMethodStringEmpty {
549+
550+
@Command(command = "", alias = "")
551+
void command1(){
552+
}
553+
554+
@Command(command = "two", alias = "twelve")
555+
void command2(){
556+
}
557+
}
558+
559+
}
560+
378561
}

spring-shell-docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*** xref:commands/exceptionhandling/resolving.adoc[]
1414
*** xref:commands/exceptionhandling/mappings.adoc[]
1515
*** xref:commands/exceptionhandling/annotation.adoc[]
16+
** xref:commands/alias.adoc[]
1617
** xref:commands/hidden.adoc[]
1718
** xref:commands/helpoptions.adoc[]
1819
** xref:commands/interactionmode.adoc[]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
[[commands-alias]]
2+
= Alias
3+
4+
ifndef::snippets[:snippets: ../../../../src/test/java/org/springframework/shell/docs]
5+
6+
It is possible to define an _alias_ for a command. This is convenient for
7+
cases where you want to create a shorter version of a command or going
8+
through a complete command rename while keeping old one temporarily in
9+
place.
10+
11+
Format for _alias_ is slighly different than a _command_. When _command_
12+
is defined as an array it's concatenated together into a single command.
13+
When _alias_ is defined as an array it's used to create a separate
14+
aliases.
15+
16+
Aliases with a plain `CommandRegistration` is simple and clear as you
17+
get exactly what you define as there's no "magic" in it.
18+
19+
[source, java, indent=0]
20+
----
21+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=builder]
22+
----
23+
24+
Defining alias with `@Command` annotation is a bit more involved as it
25+
can exist on a both class and method levels. Here are examples how it
26+
works.
27+
28+
Alias just on a method gives you _myalias_.
29+
30+
[source, java, indent=0]
31+
----
32+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=command1]
33+
----
34+
35+
Or _myalias1_ and _myalias2_ if defined as an array.
36+
37+
[source, java, indent=0]
38+
----
39+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=command2]
40+
----
41+
42+
Alias only on a class level does nothing as it's simply an instruction
43+
for annotation on a *method level if defined*.
44+
45+
[source, java, indent=0]
46+
----
47+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=command3]
48+
----
49+
50+
Alias on both class and method level combines those two together where
51+
class level works as an prefix and method level as combination of aliases.
52+
Alias on a class level is usually used together with a _command_ prefix
53+
to keep aliases on a same command level.
54+
55+
Here you'd get alias _myalias1 myalias2_.
56+
57+
[source, java, indent=0]
58+
----
59+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=command4]
60+
----
61+
62+
On a method level there's a special format, that being an *empty string*
63+
which allows you to create an alias but it only uses prefix from a
64+
class level.
65+
66+
Here you'd get alias _myalias1_.
67+
68+
[source, java, indent=0]
69+
----
70+
include::{snippets}/CommandRegistrationAliasSnippets.java[tag=command5]
71+
----

0 commit comments

Comments
 (0)