censhare uses commands to call the domain logic and comand handlers to implement domain logic. This combination is an implementation of the command design pattern
Commands
A command is used to call a particular piece of functionality by letting the system know what to do. For example:
- "Place an asset into a box"
- "Create a new asset"
A command is executed in the back end (server) and it is defined by a unique ID and a name that indicates its purpose (e.g. createNewAsset
). It typically has input parameters and it may have result data. Normally a command is used to modify data (e.g. update assets), however it is rarely used to read/query data by using Queries and Live Queries for that purpose. This concept is named "CQRS" (Command Query Responsibility Segregation).
Executing a command
- The command is called by its name
- The command input and result parameters are JSON objects. The command is executed asynchronously
- The execution method returns a promise
- The callback functions can be registered on the promise to receive the result or handle any failures
// Prepare command input parameters var params = { assetRef : asset.self, comment : dialogData.comment }; // Execute the command asynchronously csApiSession.execute("com.censhare...moveToNextWorkflowStep", params) .then( function(result) { // Promise callback: Handle command result (if any) } );
Why do we use commands
Benefits
- Decoupling from the execution context
- Similar API for various languages: Typescript, Java, REST, XSLT, ...
- Support for different transport protocols
- Support for asynchronous execution
- Support for batch or parallel execution
- Base for recovery/redo strategies
- Base for undo implementations Possible to attach progress listeners
Disadvantages
- Somewhat uncommon and cumbersome interface/ usage
- Lots of implementation classes needed (Command Handlers)
Command Handlers
A Command Handler is responsible for the execution of a command. The command states "what" to do and the Command Handler knows "how" to do it. It is used to implement the business/domain logic and it is implemented in Java on the back end (server). A Command Handler is responsible for exactly one command.
How does it work
The client executes a Command asynchronously and receives a promise. The Command request is sent via the transport protocol, e.g. HTTP.
The server dispatches the Command to a Command handler, which is responsible to execute the command and implements some functionality (domain/business logic).
The Command Handler interactions with the Query Service (e.g. to exchange information about assets), and the Query Service on its own turn interacts with the persistence layer.
In the same manner, the Command Handler interacts with the Repository (e.g. to update assets), and the Repository interacts with the persistence layer.
A Command Handler may build and use its own domain model. Additionally, when finished, the command returns a result (e.g. "OK" + optional result data). The command might trigger indirect results via Live Query updates.
Developing new/ testing & debugging
Adding input and result parameter
Usually your command requires some input data to process its business logic and also should return a result. In the next step we will add both an input parameter and a result object to our execution method.
- To add an input parameter, just add a second parameter to the method's signature.
- To return a result, just change the method's return type to the desired type.
@Execute public ResultData execute(CommandContext context, InputData input)
Both input and result objects must be ordinary Java objects (POJOs) with this prerequisites:
- They must not be primitive types.
- Their classes must be public.
- Their classes must either be top-level classes or static inner classes.
- Their classes must have either no declared constructor at all or a public default constructor.
- All fields (properties) must be public.
- All fields must be marked with the
@Key
annotation.
It is good practice to declare both input and result parameters of a Command Handler as public static inner classes within the Command Handler class itself.
We now extend our Command Handler example to accept an input parameter and return some result. The command should get two properties as input: The client's name (property myName
) and some magic number (property magicNumber
).
public static class InputData { @Key public String myName; @Key public int magicNumber; }
The command should return a greeting string ("Hello client") and the magic number, which was provided as input parameter.
public static class ResultData { @Key public String greeting; @Key public int magicNumber; }
Here is the complete CommandHandler code:
import com.censhare.support.json.ClassAnalyzer.Key; @CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { public static class InputData { @Key public String myName; @Key public int magicNumber; } public static class ResultData { @Key public String greeting; @Key public int magicNumber; } @Execute public ResultData execute(CommandContext context, InputData input) { // Process the command (business logic) using the given InputData // ... // Return the result back to the client ResultData result = new ResultData(); result.greeting = "Hello " + input.myName; result.magicNumber = input.magicNumber; return result; } }
Handling with input and result parameters on the client side
// Prepare command input parameters var params = { myName: "Arthur", magicNumber: 42 }; csApiSession.execute("com.acme.CustomCommand", params).then( function (result) { console.log(result.greeting); console.log("magic number:" + result.magicNumber); } );
Calling a command with input parameters is easy. Just create a JavaScript object having exactly the same properties as the public fields of the POJO of the Command Handler's input parameter. That object must be provided as second parameter to the execute function.
The result of the command is passed as parameter to the success callback function. Again the mapping to the result POJO is straight forward: Just access the properties by their name as defined in the Command Handlers result POJO.
In the next section we will have a closer look onto the rules, how POJOs are mapped to JavaScript objects and vice versa.
JSON/POJO conversion
All objects sent by the Server to the Client are serialized into JSON, which interpreted then as objects in JavaScript. The data sent by the Client are converted back to the appropriate Java objects.
- Data are automatically converted from JSON to Java object and vice versa
- All fields must be annotated with
@Key
- All fields must be annotated with
- Basic Java types (String, Integer, Double, Boolean, etc) become simple JSON properties
- Complex Java objects become JSON objects
- Java lists (
java.util.List
) become JSON arrays
Primitive types
Supported primitive property types are strings, booleans and numbers. The Java types string and boolean are mapped directly to their JavaScript/JSON counterparts. The Java types int
, float
and double
are all mapped to JavaScript numbers.
In the other direction (JSON to Java) the type of the field defines the target type. A JavaScript number is converted to the target type if necessary (e.g. integer to float conversion).
public static class Data { @Key public String stringProperty = "abc"; @Key public boolean booleanProperty = true; @Key public int integerProperty = 1; @Key public float floatProperty = 1.5; @Key public double doubleProperty = 1.005d; }
{ stringProperty: "abc", booleanProperty: true, integerProperty: 1, floatProperty: 1.5, doubleProperty: 1.005 }
Complex types
A property of a request or result object can not only be a primitive type, but also a complex one, i.e. another object. That way you can compound objects to arbitrary complex data structures.
Just keep in mind that:
- The properties of the sub object must be tagged with the
@Key
annotation as well. - The whole object must be representable as a valid JSON structure. Hence it must be a tree structure, not an arbitrary graph.
public static class Data { @Key public Data1 complexProperty1; @Key public Data2 complexProperty2; } public static class Data1 { @Key public int a = 1; @Key public int b = 2; } public static class Data2 { @Key public String c = "foo"; @Key public String d = "bar"; }
{ complexProperty1: { a: 1, b: 2 }, complexProperty2: { c: "foo", d: "bar" } }
Lists and arrays
You can create lists of objects as properties as well. Java lists (java.util.List
) are converted to JavaScript arrays and vice versa. To make JSON to POJO conversion work, it is necessary to initialize each list property with a concrete list type on declaration (typically ArrayList).
public static class Data { @Key public List<Entry> entries = new ArrayList<>(); } public static class Entry { @Key public int a; @Key public int b; }
{ entries: [ { a:1, b:2 } , { a:3, b:4 }, { a:5, b:6 } ] }
There are two limitations currently:
- Java arrays are not supported.
- Arrays/Lists of primitive types are not supported. As a workaround you have to wrap the list's data into an object.`
Known problems/limitations
Serialization POJO âžž JSON:
- Runtime type information is not used
- Solution: Don't declare fields as interfaces or abstract types
- "Hard coded" exceptions:
java.util.List
java.lang.Object
Deserialization JSON âžž POJO:
- Any fields which are declared as interface (e.g.
java.util.List
) cannot be instantiated (implementation class not known)- Solution: Always use ArrayList to represent JSON arrays or instantiate a field on declaration
@Key ArrayList<String> field; or @Key List<String> field = new ArrayList<>()
JSON serializer
Interface JsonSerializable
The following pattern is usually used to serialize POJOs into JSON:
Serializers.fromPojo(this).toJSON(true);
Most of the time this is used for debugging purposes, and usually it is implemented by something generic like that:
@Override public String toString() { return Serializers.fromPojo(this).toJSON(true); }
For that matter, a new interface com.censhare.api.json.JsonSerializable
it is introduced simply consisting of the method toJSON(boolean pretty)
. The method has a default implementation of the pattern mentioned above:
public interface JsonSerializable { default public String toJSON(boolean pretty) { return Serializers.fromPojo(this).toJSON(pretty); } }
So, if you inherit from this Interface, your class (with fields annotated with @Key
) will provide a toJSON()
method out of the box without the requirement to add the pattern on your own.
Example:
public abstract class LayoutEditorApplicationResultParam implements JsonSerializable { @Override public String toString() { return toJSON(true); } public final static class CreatePDFResult extends LayoutEditorApplicationResultParam { /** URL to the PDF temporary file. */ @Key public String url; } }
Important: Due the fact that this interface uses the Java 8 modifier "default", it is not possible to add this interface to censhare-LibSupport until it is moved to Java 8 as well. Therefore, it is located within the API package of the censhare-Server project.
Extended information for deserialization of JSON to POJO
censhare 5 uses heavily POJO to JSON serialzer and JSON to POJO deserializer for the communication between the back-end and client's front-end, as well as in other areas of the code.
For simplification, an implementable interface (ClassDetector) for the @Key
annotation it is introduced, to satisfy the cases where the de-/serialzier need to know which class should be used to create a new Object of a JSON structure or to tell about the concrete class of an Object to serialize it to JSON.
There are some cases that additional information are required in the deserialization process. For example, in the Flat-plan implementation it is mandatory to get the name of the command handler within the deserialization of a special field (to make a lookup in a predefined static hash-map).
For that reason, the parameter signature of the method getClassInfo
is it extended with a third parameter of type DeserializationContext
.
com.censhare.support.json.ClassAnalyzer.ClassDetector.getClassInfo(KeyInfo keyInfo, Map<String, Object> attributeMap, DeserializationContext context)
This deserialization context contains additional information, that can be used to figure out the correct class to deserialize a field to a POJO. In the context of the command handler communication, it only contains the name of the surrounding command handler for now.
Example:
- We get the following data structure delivered from the front-end. Field event is annotated by
EventInstanceDetector
public static class EventData implements JsonSerializable { /** A unique sequence id per event. front-end and back-end are using different values. */ @Key public long seq; /** Reference to the real event implementation. */ @Key(detector=EventInstanceDetector.class) public AbstractEvent event; [...] @Override public String toString() { return toJSON(true); } }
- The
EventInstanceDetector
uses the name of the command handler to find the correct implementing class of typeAbstractEvent
:
public static class EventInstanceDetector extends InstanceDetector { @Override public ClassInfo getClassInfo(KeyInfo keyInfo, Map<String,Object> attributeMap, DeserializationContext context) { String eventName = (String)attributeMap.get("name"); // The command handler name is used to get the event implementation. String commandHandlerName = context.get(DeserializationContext.commandHandlerName); Class cls = getEventClassEx(commandHandlerName, eventName); return ClassAnalyzer.getClassInfo(cls); } }
As it can be seen from the examples above, we can figure out the class-type in context of the name of the command handler.
Currently the DeserializationContext
contains only one pre-defined key DeserializationContext.commandHandlerName
.
This can be easily extended by any other type if you have the requirement for that.
Outside of the command handler context, it is possible to use the DeserializationContext
in combination with other values as well.
Reacting to errors
So far we learned how to send data to a Command handler and how to return a result. Now it's time to have a short look how to signal errors back to the client. Well, actually it's quite trivial: Just let your execution method throw a normal Java exception.
@Execute public ResultData execute(CommandContext context, InputData input) { if (input.myName == null) throw new IllegalArgumentException("Property myName not set"); ... return result; }
On the client side you can react on the error in the error callback function of the command promise:
csApiSession.execute("com.acme.CustomCommand", params).then( function (result) { // success }, function (error) { console.log("error", error); csNotify.failure('XYZ action failed', error); } );
The error object offers the following properties:
- message: The error message
- type: The full class name of the Java Exception
- stacktrace: The exception stack trace as string.
{ message: "Property myName not set", type: "java.lang.IllegalArgumentException", stacktrace: "<java stacktrace>" }
Command handler scope types
Depending on the Command Handler's purpose, the scope of a command handler can have the following types:
- Request scope or ScopeType.REQUEST - For a single method call. This is the default type.
- Conversation scope or ScopeType.CONVERSATION - For multiple method calls
Request scope
- When a scope parameter is missing, the type of the command handler scope will be ScopeType.REQUEST (default)
- Only one single execution method, the name of method is irrelevant
- The command finishes when the execution method is completed
@CommandHandler (command="com.censhare...", scope=ScopeType.REQUEST) public static final class MoveToNextWorkflowStepHandler { @Execute public CommandResult execute(CommandContext context, InputData input) { } }
Conversation scope
You can set a scope on your Command Handler, which defines the life time of the handler. If no scope is defined, the Command Handler gets the default scope, which is REQUEST. Our example Command Handler, we implemented before, was in request scope, since we did not explicitly specify any other scope. A request based Command Handler only offers one single execution method. Once the execution method has finished, the whole command is finished and the Command Handler instance is destroyed.
Sometimes it is useful to have a Command Handler, which stays alive for a longer time, and offers the possibility to allow multiple method invocations. One example are wizard like actions, where the client calls a server method and shows a dialog based on the result of that method call. Then the client calls the next method on the back-end and again may show a dialog afterwards.
To declare a conversational Command Handler, you have to mark it with the parameter scope=ScopeType.CONVERSATION
within the @CommandHandler
annotation.
Then you can mark multiple methods with the @Execute
annotation.
@CommandHandler(command = "com.acme.CustomCommand", scope=ScopeType.CONVERSATION) public class CustomCommandHandler { @Execute public ResultA methodA(CommandContext context, InputA input) { ... } @Execute public ResultB methodB(CommandContext context, InputB input) { ... } @Execute public ResultC methodC(CommandContext context, InputC input) { ... } }
How does the Command Handler know, which execution method should be executed? Since there are multiple of them, the client must explicitly call the execution method by its name when invoking the command. This can be done by using the 3 argument variant of the execute function:
var promise = csApiSession.execute("com.acme.CustomCommand", "methodA", params);
Further execution method calls can then be done using a special execute function on the promise. Note that it is not allowed to call a second execute method while the first one is still active (see section Command Handlers and multi threading to learn more about this topic). Hence, the method calls must be serialized properly using the promise success callback.
// Call methodA var promise = csApiSession.execute("com.acme.CustomCommand", "methodA", params); promise.then( function (result) { // Call methodB return promise.execute("methodB", params); } ).then( function (result) { // Call methodC return promise.execute("methodC", params); } ).then( function (result) { // Handle final result } );
Don't forget the return
inside the callback function! It's important that the callback function returns the promise it gets from the execute()
function. Otherwise, it would return no result and fulfill the promise immediately. As a consequence the next callback function would be invoked immediately without waiting for the previous Command Handler method having completed.
In theory a conversational scoped Command Handler will stay alive "forever". If the client frequently calls some execution methods on the Command Handler, the session timeout (1 hour default) will never be reached and the command stays active. But how can we then end a conversational command? That's in the responsibility of the Command handler itself. The Command Handler must complete (finish) the command explicitly by wrapping the result of an execution method with CommandResult.completed(result)
. Once this "termination" method is called by the client and has finished, the framework knows that the command is finished and destroys the Command Handler.
Let's have a look onto our previous example again:
@CommandHandler(command = "com.acme.CustomCommand", scope=ScopeType.CONVERSATION) public class CustomCommandHandler { @Execute public ResultA methodA(CommandContext context, InputA input) { ... } @Execute public ResultB methodB(CommandContext context, InputB input) { ... } @Execute public ResultC methodC(CommandContext context, InputC input) { ... ResultC result = new ResultC(...); return result; } }
Let's make methodC the termination method. The method has to be modified a bit: The return type is changed to CommandResult
and within the method body the actual result must be wrapped:
@Execute public CommandResult methodC(CommandContext context, InputC input) { ... ResultC result = new ResultC(...); // Finish command return CommandResult.completed(result); }
We will have a more detailed look into the life cycle of a Command Handler in the next section.
Command Handler Thread
Command Handlers are single-threaded. That means, only one execution method can be active at one time. Parallel execution of Command Handler methods is not supported. It's not guaranteed that consecutive method calls are all handled by the same thread, but only one execution thread can be active at one time.
The advantage of a single-threaded environment is that the developer of a Command Handler does not need to take care about concurrency. A conversational Command Handler can store state in private fields without the need to synchronize the access to those fields.
@CommandHandler(command = "com.acme.CustomCommand", scope=ScopeType.CONVERSATION) public class CustomCommandHandler { // Internal property (state) of the Command Handler private int myState = 0; @Execute public ResultA methodA(CommandContext context, InputA input) { if (myState != 0) throw new IllegalStateException(); myState = 1; ... } @Execute public ResultB methodB(CommandContext context, InputB input) { if (myState != 1) throw new IllegalStateException(); myState = 2; ... } }
The framework prevents calling methods in parallel. However such method calls are neither serialized nor synchronized in any way. Hence it's in the responsibility of the developer to avoid calling methods in parallel. If he does so, the method execution fails with an error.
Let's have a look onto the following example, which does it the wrong way! In that case methodA is executed. But immediately afterwards an attempt is made to execute methodB, without waiting for the completion of methodA. This code will fail with an error: "Illegal state of command [executing] to execute method".
var promise = csApiSession.execute("com.acme.CustomCommand", "methodA", params); // Wrong! Calling methodB without waiting for completion of methodA promise.execute("methodB", params); // Wrong! Calling methodC without waiting for completion of methodA and methodB promise.execute("methodB", params);
To do it the right way, you must chain the method calls using the promise completion callback function. MethodB is called when methodA has finished and methodC is called when methodB has finished:
var promise = csApiSession.execute("com.acme.CustomCommand", "methodA", params); promise.then( function (result) { // Call methodB when methodA has finished return promise.execute("methodB", params); } ).then( function (result) { // Call methodC when methodB has finished return promise.execute("methodC", params); } );
There are two exceptions to this general "single-thread" rule: Cancellation and notifications.
If a command gets canceled, the Command handler's cancel callback method (if present) is called asynchronously from within another thread and it may happen that any of the command's execution methods is currently active.
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { ... @Cancel public void canceled(CommandContext context) { // This code is called asynchronously from another thread! } }
Same is true if the client sends an asynchronous notification (see client notifications). In that case the notification method is called from within another thread.
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { ... @Notify public void notification(CommandContext context) { // This code is called asynchronously from another thread! } }
If you need to add cancellation or notification callbacks to your Command Handler and those methods must access internal state, synchronization is required.
Command Handler Notifications
Command Handlers have two existing directions of notifications:
- Command Handler notifies the client
- Client notifies the Command Handler
Command Handler notifies the client
In some situations it can be useful if the Command Handler is able to send information to the client while an execution method is still active. Typical use cases are:
- Sending progress information during long running execution methods.
- Requests with "push" updates. Example: Updating the result of a Live Query.
This is possible using the notification channel. Inside of an execution method of the Command Handler you can send an asynchronous notification to the client by calling the CommandContext's notify
method. It takes the notification data (an annotated POJO) as parameter.
@Execute public ResultData execute(CommandContext context) { // Send notification to client context.notify(notificationData); }
On the client side you are able to listen to notifications by adding a third callback function to the command's promise, the notification or progress callback:
var promise = csApiSession.execute(...).then( function (result) { // success }, function (error) { // error }, function (notification) { // notification } );
Here is a complete example of a Command Handler, which uses the notification channel to send progress information back to the client while its execution method is busy.
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { public static class ProgressData { @Key public String message; @Key public int percent; } @Execute public ResultData execute(CommandContext context, InputData input) throws InterruptedException { // Long running execution loop... for (int i = 1; i <= 10; i++) { Thread.sleep(1000); ProgressData progress = new ProgressData(); progress.message = "Working on item " + i; progress.percent = i * 10; // Send notification to client: progress info context.notify(Data.wrapPojo(progress)); } ... } }
The client can then listen to those progress updates and refresh its UI accordingly, e.g. update a progress bar. In this example we just dump the progress information to the console.
var promise = csApiSession.execute("com.acme.CustomCommand", params).then( function (result) { // success }, function (error) { // error }, function (progress) { // notification received (progress update) console.log(progress.message + "(" + progress.percent + ")"); } );
Client notifies the Command Handler
The client has the possibility to send asynchronous notifications to the server as well. Asynchronous notifications are useful to "intercept" long running execution methods, since the client has no other way to talk with the server while an execution method is active. An example use case is a changed filter definition, which is sent to an active query command.
A notification can be sent from the client to the server using the notify
method on the command's promise. The notify function accepts the name of the notification method and additional notification data as parameters. A notification does not return any synchronous or asynchronous result and no promise. The notify function returns immediately without any result.
var promise = csApiSession.execute("com.acme.CustomCommand", params); ... promise.notify(methodName, notificationData);
Here is a small usage example. The client calls a command, which is expected to take some time to finish its execution. While the command is active, the client sends a notification A to the Command Handler 2 seconds later. After 4 seconds the client sends an additional notification B to the command.
var promise = csApiSession.execute("com.acme.CustomCommand", params); // Send notification A some seconds later $timeout(function () { // Notification data var notification = { magicNumber: 43 }; promise.notify("notificationA", notification); }, 2000); // Send notification B some seconds later $timeout(function () { // Notification data var notification = { a: "foo", b: "bar" }; promise.notify("notificationB", notification); }, 4000);
Let's switch to the Command Handler side. To accept and handle the notifications, a callback method must be added for each type (method name) of the notification. This can be easily done by just adding a public method and mark it with the @Notify
annotation. The name of the method must be equal to the name provided by the client in the notify call: notificationA and notificationB in our example. Since notifications cannot return any result, the callback methods should have the void
result type.
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { ... public static class NotificationDataA { @Key public int magicNumber; } public static class NotificationDataB { @Key public String a; @Key public String b; } @Execute public ResultData execute(CommandContext context, InputData input) throws InterruptedException { // The command's execution method takes so much time for it's computation... ;-) Thread.sleep(30000); ... } @Notify public void notificationA(CommandContext context, NotificationDataA data) { // Received notification A. Handle it here... } @Notify public void notificationB(CommandContext context, NotificationDataB data) { // Received notification B. Handle it here... } }
Client notifications can be used for both request scoped and conversation scoped Command Handlers. For a request scoped Command Handler notifications must be sent while the handler's execution method is active. Once it has finished the command is finished as well and will not accept any further notifications. A conversational Command handler accepts notifications at any time (as long as the command is not finished). It doesn't matter if an execution method is currently active or not.
Be aware that notification methods are called asynchronously from within another thread. So if the method needs access to internal state of the Command Handler, synchronization is necessary (see section Command Handlers and multi threading for details).
Live Cycle
We have already learned about request scoped and conversation scoped Command Handlers. The main difference is that the first one stays alive for one single request, whereas the latter stays alive for multiple requests. Hence the life cycle of both handler types differs a bit.
Life cycle of request scoped Command Handler
- Client calls
csApiSession.execute(...)
- A new Command Handler instance is created by instantiating the Command Handler class using the default (empty) constructor.
- It's recommended not to implement the constructor in your Command Handler class, since nothing should be done here. Provide a special Init-method if the handler needs initialization.
- The
@Init
-Method is called if available.- You can provide an initialization method for your command handler by tagging a method with the
@Init
annotation. - The method is called exactly once immediately after creation of the handler class
- The only parameter is the CommandContext.
- Note that implementing an init method for a request scoped Command Handler does not make much sense. It's typically used for conversational handlers.
- You can provide an initialization method for your command handler by tagging a method with the
- The
@Execute
-Method is called. - The
@Release
-Method is called if available.- If a method is tagged with the
@Release
annotation, it is called immediately before the handler is destroyed. It can be used to do some cleanup work or free used resources.
- If a method is tagged with the
- The Command Handler instance is destroyed.
- Note that a Command Handler instance is used only once. When the command is finished the instance is destroyed and never used again.
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { @Init public void init(CommandContext context) { // Initialization... } @Execute public ResultData execute(CommandContext context, InputData input) { // Command execution... } @Release public void release(CommandContext context) { // Cleanup... } }
Life cycle of conversation scoped Command Handler
- Client calls
csApiSession.execute(command-name, 'methodA', ...)
- A new Command Handler instance is created by instantiating the Command Handler class using the default (empty) constructor.
- It's recommended not to implement the constructor in your Command Handler class, since nothing should be done here. Provide a special Init-method if the handler needs initialization.
- The
@Init
-Method is called if available.- You can provide an initialization method for your command handler by tagging a method with the
@Init
annotation. - The method is called exactly once immediately after creation of the handler class
- The only parameter is the CommandContext.
- You can provide an initialization method for your command handler by tagging a method with the
- The
@Execute
-Method "methodA" is called. - The client can call additional Command Handler execution methods via
promise.execute(methodName, ...)
.- It may call different execution methods.
- It's also allowed to call the same execution method multiple times.
- methodB, methodA, methodB, methodC, ...
- Termination of the command:
- Client calls the "termination" method (method, which decides to terminate/end the command).
- Or any execution method throws an exception (any error terminates the command immediately).
- The
@Release
-Method is called if available.- If a method is tagged with the
@Release
annotation, it is called immediately before the handler is destroyed. It can be used to do some cleanup work or free used resources.
- If a method is tagged with the
- The Command Handler instance is destroyed.
- Note that a Command Handler instance is used only once. When the command is finished the instance is destroyed and never used again.
@CommandHandler(command = "com.acme.CustomCommand", scope=ScopeType.CONVERSATION) public class CustomCommandHandler { @Init public void init(CommandContext context) { // Initialization... } @Execute public ResultA methodA(CommandContext context, InputA input) { // Command execution... } @Execute public ResultB methodB(CommandContext context, InputB input) { // Command execution... } @Execute public CommandResult methodC(CommandContext context, InputC input) { // Termination method ... ResultC result = new ResultC(...); // Finish command return CommandResult.completed(result); } @Release public void release(CommandContext context) { // Cleanup... } }
Canceling commands
Commands can be canceled as well. There are two possible situations:
- Explicit cancellation by the client.
- Automatic cancellation of long running commands after 1 hour of inactivity.
Explicit cancellation by the client
The client has the possibility to cancel long running commands. This can be done by invoking the cancel
method on the command's promise. In the following example a command is executed and canceled 5 seconds later.
var p = csApiSession.execute("com.acme.CustomCommand", params); // Cancel the command a few seconds later $timeout(function () { if (p.getStatus() === "executing") { p.cancel(); } }, 5000);
To make this work we can simulate a long running command by adding a simple sleep to the execution method of our request scoped Command Handler:
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { @Execute public ResultData execute(CommandContext context, InputData input) throws InterruptedException { // Simulate long running command execution... Thread.sleep(20000); ResultData result = new ResultData(); return result; } }
When the cancel request is received, the framework first sends the execution thread of the Command Handler an interrupt signal. In our example the method sleep
will throw an InterruptedException in that case.
The CommandHandler itself has the chance to react on the cancellation if required. Just add a method to the handler class and tag it with the @Cancel annotation:
@CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { ... @Cancel public void canceled(CommandContext context) { // Called when the command is canceled } }
Note that this method is called asynchronously from any thread. In particular it is not executed in the same thread, which executes the execution method of the Command Handler. Hence thread safety must be considered in that case by the developer.
Automatic cancellation by the framework
Long running commands are automatically canceled by the framework after 1 hour of inactivity. A command is considered as inactive if none of its execution methods was called or has finished within the last hour. Note that the framework only registers entry and exit of an execution method. So if an execution method is called and does not finish within 1 hour, the command will be canceled as well, although the method itself may be still busy.
However there's a way to prevent cancellation in case you plan to implement a command with long running execution methods or a conversational command with long pauses between method calls. In such cases it's necessary that the Command Handler explicitly signals activity to the framework in regular intervals. This can be done by periodically calling the method noteAccess
on the CommandContext.
@Execute public ResultData execute(CommandContext context, InputData input) { // Some long running task, which may take > 1 hour ... while (!finished) { // Do something... // Prevent cancellation. Signal activity to the framework context.noteAccess(); } ResultData result = new ResultData(); return result; }
Example: Creating a Command Handler
- Step 1: Add a Java source path reference to your widget's package JSON file.
- This will allow the framework to find your Java classes and to compile them automatically.
- Step 2: Create a Java package (folder) within your widget directory:
- As usual the package name must start with your company's inverse domain name, e.g. "com.acme."
- yourWidget/src/java/com/acme/
- Step 3: Create the CommandHandler Java class
- Step 4: Call the CommandHandler from within your widget (Typescript)
Step 1: Package JSON
Typically the Command Handler belongs to a widget or a module, therefore it should be located in the same directory tree as the widget/module within the censhare Web project.
- Create a subfolder
src/java
inside the widget folder - Add the reference to the Java source code in the
cs.pkg.json
file of the module
{ ... "java": { "sourcepath": "src/java" } }
An example of a complete package JSON file:
{ "name": "acmeCustomWidget", "since": "5.7.0", "version": "1.0.0", "styles" : ["acmeCustomWidget.scss"], "deps": [ "csTranslate" ], "implementations": [ { "type": "csWidget", "name": "acmeCustomWidget", "properties": { "contentControllerName": "acmeCustomWidgetContentController", "contentTemplateName": "acmeCustomWidgetTemplate.html", "widgetInfo": { "icon": "acme-custom-icon", "title": "acmeCustomWidget.title", "description": "acmeCustomWidget.description" } } } ], "translations": [ "translation" ], "java": { "sourcepath": "src/java" } }
Step 2: Java package folder
Create subfolders within the src/java
directory following the name parts of the desired package name of your CommandHandler. As it is convention in Java, the package should be prefixed with the inverse domain name of your company. In our example we are the fictive company Acme, hence the package name is com.acme
. Of course, you are free to add additional sub package names after the package's domain prefix, e.g. com.acme.test.something.
.
Step 3: Create the CommandHandler Java class
A Command Handler can be an arbitrary class:
- Not required to inherit from a base/abstract class
- Not required to implement a certain interface
- However it must be marked with specific annotations
- The name of the class should reflect the command's use case (business logic)
- In our example we just use the neutral name
CustomCommandHandler
- In our example we just use the neutral name
package com.acme; import com.censhare.server.support.api.impl.CommandAnnotations.CommandHandler; @CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { }
- You can create an own file for each of your CommandHandlers. However, if you like to group multiple commands (CommandHandlers) into one Java file, its good practice to implement the handlers as public static inner classes.
- The class must be annotated with
@CommandHandler
- The class annotation must contain the unique command id:
- The command ID is used by the client to call the command.
- In principle you are free to choose any name here.
- However, it is best practice to use the name of the CommandHandler without suffix "Handler" and
- prefix it with the Java package name.
- In our example:
com.acme.CustomCommand
- Put the class into your source folder (
src/java/com/acme
in our example)
Method annotations:
@Execute
- "execution" method of a command@Init
- "initialization" method of a command. It is called immediately after the construction of the command handler. An initialization method should be used in favor than doing initialization work in the constructor, since all dependencies are guaranteed to be injected correctly when this method is called by the framework.@Release
- "release" method of a command. It's called if the corresponding command has finished. It can be used to do some cleanup work in the command handler.@Notify
- "notification" method of a command@Cancel
- "cancel" method of a command. It is called if the corresponding command was canceled asynchronously. Note that such methods are typically called from another thread, i.e. not from within the command's executor thread
Note! The command handler class can have multiple initialization, release or cancel methods. The order in which those are called is not defined.
Add an execution method
package com.acme; import com.censhare.server.support.api.CommandContext; import com.censhare.server.support.api.impl.CommandAnnotations.CommandHandler; import com.censhare.server.support.api.impl.CommandAnnotations.Execute; @CommandHandler(command = "com.acme.CustomCommand") public class CustomCommandHandler { @Execute public void execute(CommandContext context) { // Empty for now } }
- A normal Command Handler needs exactly one execution method (Conversational Command Handlers may have more than one execution methods. We will talk about them later).
- The execution method is called when the command is executed. Its purpose is to contain the actual business logic of the Command Handler.
- The execution method must be marked with the annotation
@Execute
. - It can have any name.
- It must contain the
CommandContext
as first parameter of its signature. - It may have an additional input parameter and/or a result parameter. In our example we do not add input and result parameters for now.
Step 4: Call the Command Handler from within your widget
csApiSession.execute("com.acme.CustomCommand").then( function (result) { // Promise success callback: Handle command result (if any) }, function (error) { // Promise error callback: Handle error } );
- Execute your command with the
execute
function on thecsApiSession
. - Pass the ID of the command as first parameter to the execution method.
- Commands are executed asynchronously.
- The execution method immediately returns a promise.
- Callback functions can be registered on the promise to receive the result or handle failures.
If csApiSession
is not yet part of your widget, it can be easily injected.
m.controller("acmeCustomWidgetContentController", ['$scope', 'widgetInstance', 'csApiSession', function ($scope, widgetInstance, csApiSession) { ... }] );
Read more about the JSON serialization and POJO classes here.
Note! The list of existing command handlers must be checked, before developing a new one. The list can be found here.
Note! Some general purpose command handlers have wrapper functions in csApiSession.
Read more about csApiSession
.
Existing Command Handlers
The censhare Admin client provides a tool for listing all existing command handlers. For more information on this action, see listing modules information.
Command Handlers for Asset Management
Asset from template
- Command Handler to create a new asset by duplicating a template asset
com.censhare.api.dam.assetmanagement.createFromTemplate
- Prepare for creating new asset from template
com.censhare.api.dam.assetmanagement.createFromTemplate.prepare
Asset Deletion (Trash)
- Triggered when an asset is moved to trash
com.censhare.api.dam.assetmanagement.trash.moveto
- Triggered when an asset is recovered from trash
com.censhare.api.dam.assetmanagement.trash.removefrom
Asset Preview Handlers
- Replaces the preview and thumbnail storage item of a given asset with the preview of another asset and creates a child relation to the preview asset.
com.censhare.api.dam.assetmanagement.assetPreviewFromAsset
- Used to add a preview to an existing asset, from an external file via drag and drop to the Preview Widget. Initially , it creates a new asset from the file, and then it uses it to assign the preview to the existing asset.
com.censhare.api.dam.assetmanagement.assetPreviewFromFile
Filters
- Fires when creating a new relation from a widget. It creates filter by eliminating the already related assets, and returning all available assets for the relation selection.
com.censhare.api.dam.assetmanagement.createFilter
Re-archiving
- Asset handler for re-archiving
com.censhare.api.dam.assetmanagement.rearchive
Reference
- Creates a references between an existing asset and a new asset created from file. For example: Creating a new 'Snippet' from file within the 'View Snippet' widget that appears in the page (assetModuleWorkspaceViewSlot`) of a Slot asset.
com.censhare.api.dam.assetmanagement.reference.formFile
Relations
- Creates a new relation between two assets.
com.censhare.api.dam.assetmanagement.relation.create
- Creates a relation between an existing asset and a new asset created from file. For example: Creating a new asset from file from the 'Assignments' widget from the page (assetImage`) of an Image asset.
com.censhare.api.dam.assetmanagement.relation.formFile
- Removes a relation between two assets. For example: Clicking on the 'Remove relation' button next to a relation, on the 'Assignments' widget.
com.censhare.api.dam.assetmanagement.relation.delete
- Allows to modify the relation metadata. It can be triggered with the option 'Edit Relation Metadata', that is available on the 'Grouped Related Asset Widget' widget.
com.censhare.api.dam.assetmanagement.relation.modifyFeature
Storage Items
- Allows you to download specified storage items from a list of assets.
com.censhare.api.dam.assetmanagement.downloadStorageItems
- Replace or set master storage item
com.censhare.api.dam.assetmanagement.replacemasterstorageitem
Tasks
- Asset handler to complete task, sets completion state to 100%
com.censhare.api.dam.assetmanagement.completeTask
- Asset handler to re-open task, removes completion state
com.censhare.api.dam.assetmanagement.reopenTask
Versions
- Command handler that restores a history version of an asset. This option is available in the `History widget.
com.censhare.api.dam.assetmanagement.restoreVersion
Workflow
- Asset handler for moving the workflow to the next step.
com.censhare.api.dam.assetmanagement.moveToNextWorkflowStep
- Returns true if a next workflow step exists and the next workflow step has a default workflow target set.
com.censhare.api.dam.assetmanagement.hasNextWorkflowStep
- Asset handler to send the sequence of all workflow step from current workflow to the client.
com.censhare.api.dam.assetmanagement.workflowStateProgres
- Asset handler for worst workflow.
com.censhare.api.dam.assetmanagement.worstworkflowData
- Asset handler for setting workflow and workflow step. optionally adds a comment to the workflow.
com.censhare.api.dam.assetmanagement.setWorkflow