...
Status | ||||
---|---|---|---|---|
|
Overview
A Talon XVM has the ability to discover annotated 'Command' methods provided by an application. Such commands can be invoked remotely by tools in an out of band fashion with an application's message driven event handlers to perform administrative functions. The command framework is designed to work in the context of command line tools, guis or for administrative applications. A command method is identified by annotating it with a @Command annotation, and its parameters are exposed by annotating its parameters with either @Option or @Argument annotations which allow allows commands to be invoked by passing the arguments in a command line :format.
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
@App public void@Command(name MyApp {= "resetOrderStats", @AppStat(name = "Orders Received") private volatile long numOrdersReceived description="Resets 0;the number of orders @AppStat(nameprocessed") public String resetOrderStats(@Option(shortForm = "Orders Processed"'v', longForm ="verbose", defaultValue="false") private volatile long numOrdersProcessed = 0; /** * Gets the number of orders processed. * boolean verbose, * @return the number of orders processed */ @Command @Argument(name = "resetStatsnewOrderReceivedCount"), position=1) description="Resets the number of orders processed") public final long resetStats() { long numOrdersReceivednewNumOrdersReceived, = 0; numOrdersProcessed = 0; } /** * Handles a new order. @Argument(name = */"newOrderProcessingCount", position=2) @EventHandler public final void onOrder(NewOrderMessage newOrder) { numOrdersReceived++; // ... do some processing long numOrdersProcessed++newNumOrdersProcessed) { ... } } |
Command Annotations
Note |
---|
TODO Describe @Command, @Option, @Argument, and @RemainingArgs |
Command Invocation
Escaping
Commands are invoked on the server using a command line style interface in which parameters are passed to the command being executing as option or argument token separated by one or more whitespace characters. The rules for escaping are straightforward:
- To pass a string with whitespace as a single token it must be wrapped in quotes for example "My Text" so that the parser can tell that it is one argument rather than two arguments.
- Quotes within a quoted string must be escaped, so to represent an argument "My Text" with the surrounding quotes being passed in the parameter escaped quotes must be used: "\"My Text\""
- Because the command parser treats any token starting with '-' as an option negative leading dash characters must be escaped by prefixing them with a '\', for example \-3.1456.
The following simple command serves to demonstrate when escaping is necessary. This command prints a given line to either System.err or System.out:
...
@Command
public println(@Argument(position= 1, name="text") String line,
@Argument(position= 2, name="errorCode", required=false) int errorCode,
@Option(shortForm ='e', longForm="error", boolean err) {
if(err) {
System.err.println("ERROR [" + errorCode + "]:" + line);
}
else {
System.out.println(line);
}
}
Passing A String with Spaces
Code Block | ||
---|---|---|
| ||
invoke myVM myApp println "My String" |
Code Block | ||
---|---|---|
| ||
instruct the myApp app in the myVM xvm to println with "My String" |
Code Block | ||
---|---|---|
| ||
app.invoke("println \"My String\") |
The above will result in the following being printed to System.out in the App:
No Format |
---|
My String |
Passing A String with Quotes
Note that below we escape the quote characters within the quoted string:
Code Block | ||
---|---|---|
| ||
invoke myVM myApp println -e "\"My Error\"" |
Code Block | ||
---|---|---|
| ||
instruct the myApp app in the myVM xvm to println with -e "\"My Error\"" |
Code Block | ||
---|---|---|
| ||
app.invoke("println -e \"\\"My String\\"\") |
All of the above will result in the following being printed to System.error in the App (with the quotes preserved). Not that in the java case that the extra escapes are needed to escape the '\' character.
No Format |
---|
ERROR [0]: "My Error" |
Passing Argument Starting With A Dash
Note that below we escape the quote characters within the quoted string:
Code Block | ||
---|---|---|
| ||
invoke myVM myApp println -e \--BAD_TIMES-- \-10 |
Code Block | ||
---|---|---|
| ||
instruct the myApp app in the myVM xvm to println with -e \--BAD_TIMES-- \-10 |
Code Block | ||
---|---|---|
| ||
app.invoke("println -e \--BAD_TIMES-- \-10) |
All of the above will result in the following being printed to System.error in the App (with the quotes preserved)
No Format |
---|
ERROR[-10]: --BAD_TIMES-- |
Supported Types for Options, Arguments And Return values:
The following types are returned when describing the command usage. In Json they are represented as the name from the enumeration below. The expectation is that tools will use this options in forms.
Code Block | ||||
---|---|---|---|---|
| ||||
Note | ||||
Because command handlers are not executed on the application's business logic thread they are not allowed to touch application state. In general applications that intend to pass values back to command handlers should do so through member variables that are not part of the applications HA state that are declare as volatile. Alternatively, a command handler may inject a message into the application to perform logic that works with application state, but this is considered advanced usage as doing so is not a fault tolerant and blurs the lines between application and administrative traffic. |
Annotating Command methods
At its simplest, a method that is composed of primitive arguments needs only to be annotated with a Command annotation to expose it as a command that can be invoked remotely.
Code Block | ||||
---|---|---|---|---|
| ||||
@App
public void MyApp {
@AppStat(name = "Orders Processed")
private volatile long numOrdersProcessed = 0;
/**
* Resets the number of orders processed
*
* @param newNumOrdersProcessed The new number of orders processed
* @return The previous number of orders
*/
@Command
public int resetOrdersProcessed(long newNumOrdersProcessed,
boolean verbose) {
long prevNumOrdersProcessed = numOrdersProcessed;
numOrdersProcessed = numOrdersProcessed;
String result = "Reset Orders Processed: " + prevNumOrdersProcessed + "->" + numOrdersProcessed;
if(verbose) {
System.out.println(result);
}
return result;
}
/**
* Handles a new order.
*/
@EventHandler
public final void onOrder(NewOrderMessage newOrder) {
numOrdersReceived++;
// ... do some processing
numOrdersProcessed++
}
} |
The above method can than be invoked via the Robin api as:
Code Block | ||
---|---|---|
| ||
app.invokeCommand("resetOrdersProcessed 0 true") |
... the command name is identified by the name of the method and each parameter in the signature is treated as an argument to the command.
@Command
Above we gave a simple example of using the Command annotation to identify a command. The command can be further described by using the following command annotation elements
Parameter | Type | Description | Default |
---|---|---|---|
aliases | String [] | Returns the aliases used to invoke this command. This can be useful in cases where a command's | none |
description | String | A description of the command. This is used by tools to expose usage information for the command. It is a good idea to provide a description on commands as it provides tools with the ability to provide | none |
name | String | Returns the name of the command. If this parameter is omitted on an annotated Method then the name will default to the method name. | none |
parseOptions | boolean | Whether or not the command parser should attempt to parse options or if this command is | true |
@Argument
The @Argument annotation can be applied to method parameters to describe and constrain arguments.
Parameter | Type | Description | Default |
---|---|---|---|
name | String | A short name for the argument. This is used to identify the argument when displaying command line usage. | none |
description | String | A brief description desribing what should be supplied for this argument. This is used to describe the argument when displaying command line usage | none |
defaultValue | String | The argument's default value (if not a required argument. This value is converted from the String supplied to the argument's type. | null |
required | boolean | Whether or not the argument is required. It is illegal to position a required argument after an optional one ... optional arguments. | true |
position | int | The position of the argument on the command line. The 1 based position of the argument for command line invocation. | none |
validOptions | String [] | Indicates the set of permissible values for the argument. | NULL |
Below is an example of how an Argument annotation may be used:
Code Block | ||||
---|---|---|---|---|
| ||||
@Command(name = "resetOrderStats", <documentation>Enumerates the valid types for command arguments</documentation> <const name="BOOLEAN" value="1" />description="Resets the number of orders processed") public int resetOrdersProcessed(@Option(shortForm = 'v', longForm ="verbose", defaultValue="false") <const name="BYTE" value="2" /> <const name="CHAR" value="3" /> boolean <const name="SHORT" value="4" />verbose, <const name="INT" value="5" /> <const name="LONG" value="6" /> @Argument(name = "newOrderProcessingCount", position=1) <const name="FLOAT" value="7" /> <const name="DOUBLE" value="8" /> <const name="DATE" value="9" />long newNumOrdersProcessed) |
@Option
The @Option annotation can be applied to method parameters. An Option differs from an argument in that it is invoked using a command line switch. Options are useful in cases where a command method signature needs to be changed as scripts written against the old command signature will continue to function.
Parameter | Type | Description | Default |
---|---|---|---|
shortForm | char | The short (switch e.g. -o) form for the option. | none |
longForm | String | The long form for the argument (e.g. --option)). | none |
description | String | A brief description of the Option. | |
defaultValue | String | The option's default value. When the option is not specified the option will be set to this value. | null |
required | boolean | Whether or not the option is required. If the option is not specified but there is a default value, then the default will be used, otherwise it is an error. | true |
validOptions | String [] | Indicates the set of permissible values for the option. | NULL |
Below is an example of how an Argument annotation may be used:
Code Block | ||||
---|---|---|---|---|
| ||||
@Command(name = "resetOrderStats", <const namedescription="STRING" value="10" /> </enum> | ||||
Code Block | ||||
| ||||
<enum name="SrvMonOptionType" type="int">Resets the number of orders processed") public int resetOrdersProcessed(@Option(shortForm = 'v', longForm ="verbose", defaultValue="false") <documentation>Enumerates the valid types for command options</documentation> <const name="BOOLEAN" value="1" /> boolean verbose, <const name="BYTE" value="2" /> <const name="CHAR" value="3" /> <const name="SHORT" value="4" /> @Argument(name = <const name="INT" value="5" />"newOrderProcessingCount", position=1) <const name="LONG" value="6" /> <const name="FLOAT" value="7" /> <const name="DOUBLE" value="8" /> <const name="DATE" value="9" />long newNumOrdersProcessed) |
Supported Types for Options, Arguments And Return values:
The following types are returned when describing the command usage. In Json they are represented as the name from the enumeration below. The expectation is that tools will use this options in forms.
Valid Arguments and Options types:
- boolean or Boolean
- byte or Byte
- char or Char
- short or Short
- int or Integer
- long or Long
- float or Float
- double or Double
- date or Date
- String
- Enumerations (converted to/from a String when sent over the wire).
Valid return types
All of the support argument types above and void.
Registering Command Handlers with the runtime.
@AppCommandHandlerContainersAccessor
Any @Command annotated method in the main application class will be discovered by a Talon XVM. If additional classes in your application contain @Command methods, they can be exposed to the Talon server using the @AppCommandHandlerContainersAccessor annotation which should add all objects that should be introspected for command handlers.
Example:
Code Block | ||||
---|---|---|---|---|
| ||||
@AppHAPolicy(HAPolicy.EventSourcing) public static class MyApp { MyOtherClass someOtherClass = new MyOtherClass(); @AppCommandHandlerContainersAccessor <const name="STRING" value="10" /> public void getCommandHandlers(Set<Object> containers) { </enum> | ||||
Code Block | ||||
| ||||
containers.add(someOtherClass ); } } <enum name="SrvMonReturnType" type="int">class MyOtherClass { volatile int count = 0; <documentation>Enumerates theMyOtherClass() valid{ types for command} return values</documentation> <const name="BOOLEAN" value="1" />@Command(name = "getCount", <const name description="BYTE" value="2" /> Returns a counter value") public int getCount() { <const name="CHAR" value="3" /> <const name="SHORT" value="4" /> <const name="INT" value="5" /> <const name="LONG" value="6" /> <const name="FLOAT" value="7" /> <const name="DOUBLE" value="8" />return count; } } |
Configuration Discovery in Hornet
For Topic Oriented Applications, any @Managed object will be introspected for @Command methods. See ManagedObjectLocator. The DefaultManagedObjectLocator for Hornet calls TopicOrientedApplication.addAppCommandHandlerContainers(Set), so unless your application provides its own managed object locator, additional configured containers can be added by overriding addConfiguredContainers():
Code Block | ||||
---|---|---|---|---|
| ||||
@AppHAPolicy(HAPolicy.EventSourcing) public static class MyApp { MyOtherClass someOtherClass = new MyOtherClass(); @Override public void addAppCommandHandlerContainers(Set<Object> containers) { containers.add(someOtherClass ); } } class MyOtherClass { volatile int count = 0; MyOtherClass() { } @Command(name = "getCount", <const name="DATE" value description="9"Returns />a counter value") public int getCount() { <const name="STRING" value="10" /> return count; } </enum> |
...
} |
Invoking Annotated Commands
Annotated command handlers can be discovered and invoked by deployment tools such as Robin and Lumino via admin connections made to the XVM in which the application is running.
Examples / Appendix
A Fully Annotated Command
The below command handler exercises all of the
Code Block | ||||
---|---|---|---|---|
| ||||
@Command(name = "testCommand", aliases = { "TestCommandAlias" }, description = "A command that exercises all of the invocation APIs") public String testCommand(@Option(shortForm = 'b', longForm = "byteOption", required = true, description = "TestsA a byte Option", validOptions = { "0,", "1" }, defaultValue = "0") byte aByteOption, @Option(shortForm = 'c', longForm = "charOption", required = true, description = "TestsA a char Option", validOptions = { "a,", "b", "c" }, defaultValue = "b") char aCharOption, @Option(shortForm = 'n', longForm = "shortOption", required = true, description = "TestsA a short Option", validOptions = { "399,", "300" }, defaultValue = "399") short aShortOption, @Option(shortForm = 'i', longForm = "intOption", required = true, description = "TestsAn a int Option", validOptions = { "20000,", "200000" }, defaultValue = "200000") int aIntOption, @Option(shortForm = 'l', longForm = "longOption", required = true, description = "TestsA a long Option", validOptions = { "100000000,", "2000000000" }, defaultValue = "100000000") long aLongOption, @Option(shortForm = 'f', longForm = "floatOption", required = true, description = "TestsA a float Option", validOptions = { "0.0,", "-2.0" }, defaultValue = "0.0") float aFloatOption, @Option(shortForm = 'd', longForm = "doubleOption", required = true, description = "TestsA a double Option", validOptions = { "0.0,", "1.2" }, defaultValue = "1.2") double aDoubleOption, @Option(shortForm = 's', longForm = "stringOption", required = true, description = "TestsA a String Option", validOptions = { "Foo,", "Bar" }, defaultValue = "Foo") String aStringOption, @Option(shortForm = 'm', longForm = "currencyOption", required = true, description = "TestsA a Currency Option", validOptions = { "USD,", "JPY" }, defaultValue = "JPY") Currency aCurrencyOption, @Option(shortForm = 'e', longForm = "enumOption", required = true, description = "TestsAn a Enum Option", defaultValue = "PrettyPrint") JsonPrettyPrintStyle aEnumOption, @Argument(position = 1, required = true, name = "aByteArgument", description = "TestsA a byte Arguement", validOptions = { "0,", "1" }, defaultValue = "0") byte aByteArgument, @Argument(position = 2, required = true, name = "aCharArgument", description = "TestsA a char Arguement", validOptions = { "a,", "b", "c" }, defaultValue = "a") char aCharArgument, @Argument(position = 3, required = true, name = "aShortArgument", description = "TestsA a short Arguement", validOptions = { "399,", "300" }, defaultValue = "300") short aShortArgument, @Argument(position = 4, required = true, name = "aIntArgument", description = "TestsAn a int Arguement", validOptions = { "20000,", "200000" }, defaultValue = "200000") int aIntArgument, @Argument(position = 5, required = true, name = "aLongArgument", description = "TestsA a long Arguement", validOptions = { "100000000,", "2000000000" }, defaultValue = "100000000") long aLongArgument, @Argument(position = 6, required = true, name = "aFloatArgument", description = "TestsA a float Arguement", validOptions = { "0.0,", "-2.01" }, defaultValue = "-2.01") float aFloatArgument, @Argument(position = 7, required = true, name = "aDoubleArgument", description = "TestsA a double Arguement", validOptions = { "0.0,", "1.2" }, defaultValue = "0.0") double aDoubleArgument, @Argument(position = 8, required = true, name = "aStringArgument", description = "TestsA a String Arguement", validOptions = { "Foo,", "Bar" }, defaultValue = "Foo") String aStringArgument, @Argument(position = 9, required = true, name = "aCurrencyArgument", description = "TestsA a Currency Arguement", validOptions = { "USD,", "JPY" }, defaultValue = "USD") Currency aCurrencyArgument, @Argument(position = 10, required = true, name = "aEnumArgument", description = "TestsAn a Enum Arguement", defaultValue = "Minimal") JsonPrettyPrintStyle aEnumArgument) throws Exception { return "Received Arguments: " + Arrays.asList(new Object[] { aByteOption, aCharOption, aShortOption, aIntOption, aLongOption, aFloatOption, aDoubleOption, aStringOption, aCurrencyOption, aEnumOption, aByteArgument, aCharArgument, aShortArgument, aIntArgument, aLongArgument, aFloatArgument, aDoubleArgument, aStringArgument, aCurrencyArgument, aEnumArgument }).toString(); } |
Usage as Printed By SrvMonUtil
No Format |
---|
testCommand A command that exercises all of the invocation APIs Usage: testCommand -b -c -n -i -l -f -d -s -m -e <aByteArgument> <aCharArgument> <aShortArgument> <aIntArgument> <aLongArgument> <aFloatArgument> <aDoubleArgument> <aStringArgument> <aCurrencyArgument> <aEnumArgument> <-b|--byteOption> <1|0,> Tests a byte Option default='0' <-c|--charOption> <a,|b|c> Tests a char Option default='b' <-n|--shortOption> <300|399,> Tests a short Option default='399' <-i|--intOption> <200000|20000,> Tests a int Option default='200000' <-l|--longOption> <100000000,|2000000000> Tests a long Option default='100000000' <-f|--floatOption> <0.0,|-2.0> Tests a float Option default='0.0' <-d|--doubleOption> <0.0,|1.2> Tests a double Option default='1.2' <-s|--stringOption> <Bar|Foo,> Tests a String Option default='Foo' <-m|--currencyOption> <JPY|USD,> Tests a Currency Option default='JPY' <-e|--enumOption> Tests a Enum Option default='PrettyPrint' [aByteArgument: <1|0,> Tests a byte Arguement default='0'] [aCharArgument: <a,|b|c> Tests a char Arguement default='a'] [aShortArgument: <300|399,> Tests a short Arguement default='300'] [aIntArgument: <200000|20000,> Tests a int Arguement default='200000'] [aLongArgument: <100000000,|2000000000> Tests a long Arguement default='100000000'] [aFloatArgument: <0.0,|-2.01> Tests a float Arguement default='-2.01'] [aDoubleArgument: <0.0,|1.2> Tests a double Arguement default='0.0'] [aStringArgument: <Bar|Foo,> Tests a String Arguement default='Foo'] [aCurrencyArgument: <JPY|USD,> Tests a Currency Arguement default='USD'] [aEnumArgument: Tests a Enum Arguement default='Minimal'] |
Json Command Description:
Code Block | ||
---|---|---|
| ||
"name": "testCommand", "aliases": [ "TestCommandAlias" ], "description": "A command that exercises all of the invocation APIs", "additionalArguments": false, "arguments": [ { "position": 1, "required": true, "type": "BYTE", "name": "aByteArgument", "defaultValue": "0", "validValues": [ "1", "0," ], "description": "Tests a byte Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 2, "required": true, "type": "CHAR", "name": "aCharArgument", "defaultValue": "a", "validValues": [ "a,", "b", "c" ], "description": "Tests a char Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 3, "required": true, "type": "SHORT", "name": "aShortArgument", "defaultValue": "300", "validValues": [ "300", "399," ], "description": "Tests a short Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 4, "required": true, "type": "INT", "name": "aIntArgument", "defaultValue": "200000", "validValues": [ "200000", "20000," ], "description": "Tests a int Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 5, "required": true, "type": "LONG", "name": "aLongArgument", "defaultValue": "100000000", "validValues": [ "100000000,", "2000000000" ], "description": "Tests a long Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 6, "required": true, "type": "FLOAT", "name": "aFloatArgument", "defaultValue": "-2.01", "validValues": [ "0.0,", "-2.01" ], "description": "Tests a float Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 7, "required": true, "type": "DOUBLE", "name": "aDoubleArgument", "defaultValue": "0.0", "validValues": [ "0.0,", "1.2" ], "description": "Tests a double Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 8, "required": true, "type": "STRING", "name": "aStringArgument", "defaultValue": "Foo", "validValues": [ "Bar", "Foo," ], "description": "Tests a String Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 9, "required": true, "type": "STRING", "name": "aCurrencyArgument", "defaultValue": "USD", "validValues": [ "JPY", "USD," ], "description": "Tests a Currency Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "position": 10, "required": true, "type": "STRING", "name": "aEnumArgument", "defaultValue": "Minimal", "description": "Tests a Enum Arguement", "_xFieldBitmask_": [ 0 ], "xRogType": 0 } ], "options": [ { "shortForm": "b", "required": true, "type": "BYTE", "longForm": "byteOption", "defaultValue": "0", "validValues": [ "1", "0," ], "description": "Tests a byte Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "c", "required": true, "type": "CHAR", "longForm": "charOption", "defaultValue": "b", "validValues": [ "a,", "b", "c" ], "description": "Tests a char Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "n", "required": true, "type": "SHORT", "longForm": "shortOption", "defaultValue": "399", "validValues": [ "300", "399," ], "description": "Tests a short Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "i", "required": true, "type": "INT", "longForm": "intOption", "defaultValue": "200000", "validValues": [ "200000", "20000," ], "description": "Tests a int Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "l", "required": true, "type": "LONG", "longForm": "longOption", "defaultValue": "100000000", "validValues": [ "100000000,", "2000000000" ], "description": "Tests a long Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "f", "required": true, "type": "FLOAT", "longForm": "floatOption", "defaultValue": "0.0", "validValues": [ "0.0,", "-2.0" ], "description": "Tests a float Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "d", "required": true, "type": "DOUBLE", "longForm": "doubleOption", "defaultValue": "1.2", "validValues": [ "0.0,", "1.2" ], "description": "Tests a double Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "s", "required": true, "type": "STRING", "longForm": "stringOption", "defaultValue": "Foo", "validValues": [ "Bar", "Foo," ], "description": "Tests a String Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "m", "required": true, "type": "STRING", "longForm": "currencyOption", "defaultValue": "JPY", "validValues": [ "JPY", "USD," ], "description": "Tests a Currency Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 }, { "shortForm": "e", "required": true, "type": "STRING", "longForm": "enumOption", "defaultValue": "PrettyPrint", "description": "Tests a Enum Option", "_xFieldBitmask_": [ 0 ], "xRogType": 0 } ], "returnType": "VOID", "_xFieldBitmask_": [ 0 ], "xRogType": 0 } |
...