Actions & Action Handlers
Overview
The client and the server communicate bidirectionally by sending actions via JSON-RPC. In addition, they are also used for the internal event flow in both the GLSP server and the GLSP client. Any service, mouse tool, etc. can issue actions by invoking the action dispatcher, either on the client or the server.
The action dispatcher – there is one on the client and one on the server – is the central component responsible for dispatching actions to their designated action handlers.
When the dispatcher receives a new action for dispatching, it determines whether it should be dispatched to the internal action handlers only or submitted to the opposite component via JSON-RCP (server or client), based on the registered handlers on the server or the client.
The dispatcher distinguishes between notifications and request-response action pairs. Notification actions are one-way actions transferred between client and server. This means when the action dispatcher dispatches a notification it does not wait for a response and directly continues with dispatching the next incoming action. Request actions are typically issued by the GLSP client and can be used to block client-side action dispatching until the server has sent a corresponding response action.
GLSP defines the standard action types of the graphical language server protocol. However, adopters can add new custom action types. Besides, adopters can replace and extend existing, or add additional action handlers for standard or custom action types.
To do that the following steps have to be performed:
- Create a new action specification by providing a corresponding
Action
implementation - Create a new action handler for the newly created action type by providing a implementation of the
ActionHandler
interface - Configure the new action type and handler in the DI module.
Action specification
Adopters can declare new custom actions by providing an implementation for the Action
interface (resp. base class).
Java GLSP Server
public class MyCustomAction extends Action {
public static final String KIND= "myCustomKind";
private String additionalInformation;
public MyCustomAction() {
super(KIND);
}
public String getAdditionalInformation() { return additionalInformation; }
public void setAdditionalInformation(final String additionalInformation) {
this.additionalInformation = additionalInformation;
}
}
GLSP Client/Node GLSP Server
export interface MyCustomAction extends Action {
kind: typeof MyCustomAction.KIND;
additionalInformation: string;
}
export namespace MyCustomAction {
export const KIND = 'myCustomKind';
export function is(object: any): object is MyCustomAction {
return (Action.hasKind(object, KIND) && hasStringProp(object, 'additionalInformation'));
}
export function create(options: { additionalInformation?: string }): MyCustomAction {
return {
kind: KIND,
...options
};
}
}
Each action specification has a unique “kind” and can optionally declare additional data properties. We recommend defining the action kind as a static constant of the implementing class so that it can be accessed from other places, e.g. when registering the handler. Note that action instances need to be serializable to JSON. Therefore the class should only contain plain data properties and no additional business logic. In addition, references to graphical model elements should be done by id.
If an action is interchanged between client and server both need to provide the corresponding action definition.
Request-Response Actions
If the client should be able to dispatch the new action as a blocking request, the action specification class has to implement or extend RequestAction
.
Java GLSP Server
public class MyCustomRequestAction extends RequestAction<MyCustomResponseAction> {
public static final String KIND= "myCustomRequest";
private String additionalInformation;
public MyCustomRequestAction() {
super("my.custom.kind");
}
public String getAdditionalInformation() { return additionalInformation; }
public void setAdditionalInformation(final String additionalInformation) {
this.additionalInformation = additionalInformation;
}
}
GLSP Client/Node GLSP Server
export interface MyCustomAction extends RequestAction<MyCustomResponseAction> {
kind: typeof MyCustomAction.KIND;
additionalInformation: string;
}
export namespace MyCustomAction {
export const KIND = 'myCustomKind';
export function is(object: any): object is MyCustomAction {
return (RequestAction.hasKind(object, KIND) && hasStringProp(object, 'additionalInformation'));
}
export function create(options: { additionalInformation?: string, requestId?: string }): MyCustomAction {
return {
kind: KIND,
requestId: '',
...options
};
}
}
Each request action has a “requestId” and defines its response action as a type parameter. Of course, the response action specification has to be specified as well:
Java GLSP Server
public class MyCustomResponseAction extends ResponseAction {
public static final String KIND = "myCustomResponse";
public MyCustomResponseAction() {
super(KIND);
}
}
GLSP Client/Node GLSP Server
export interface MyCustomResponseAction extends ResponseAction {
kind: typeof MyCustomResponseAction.KIND;
}
export namespace MyCustomResponseAction {
export const KIND = 'myCustomResponse';
export function is(object: any): object is SetContextActions {
return Action.hasKind(object, KIND);
}
export function create(options: { responseId?: string } = {}): SetContextActions {
return {
kind: KIND,
responseId: '',
...options
};
}
}
The client can dispatch a request action either in blocking fashion awaiting the response:
@inject(TYPES.IActionDispatcher) protected actionDispatcher: GLSPActionDispatcher;
…
const response = await this.actionDispatcher.request(MyCustomRequestAction.create({ additionalInformation: "info" }));
// response is of type MyCustomResponseAction
or simply dispatch the action as non-blocking notification:
@inject(TYPES.IActionDispatcher) protected actionDispatcher: GLSPActionDispatcher;
…
this.actionDispatcher.dispatch(MyCustomRequestAction.create({ additionalInformation: "info" }));
Response actions don’t necessarily have to be part of a response-request action pair and can also be dispatched without a preceding request action.
Implementing an Action Handler (GLSP Server)
To create a new action handler, a class that implements the ActionHandler
interface has to be created.
In general, an action handler can handle one or more action kinds.
However, handling multiple action kinds is typically reserved for rather uncommon edge cases.
Therefore, the Java GLSP server provides an abstract base class that is designed for the single-action-kind-per-handler use case.
Java GLSP Server
public class MyCustomActionHandler extends AbstractActionHandler<MyCustomResponseAction> {
@Override
protected List<Action> executeAction(final MyCustomResponseAction actualAction) {
// implement your custom logic to handle the action
// Finally issue response actions
// If no response actions should be issued 'none()' can be used;
return listOf(new MyCustomResponseAction());
}
}
Node GLSP Server
@injectable()
export class MyCustomActionHandler implements ActionHandler {
actionKinds = [MyCustomRequestAction.KIND];
execute(action: MyCustomRequestAction): MaybePromise<Action[]> {
// implement your custom logic to handle the action
// Finally issue response actions
// If no response actions should be issued '[]' can be used;
return [new MyCustomResponseAction()];
}
}
The executeAction()
method has to be implemented to provide the custom logic of your action handler.
It returns a set of response actions that should be dispatched after the handler execution.
Next, the custom handler has to be configured in the DiagramModule
:
Java GLSP Server
@Override
protected void configureActionHandlers(final MultiBinding<ActionHandler> binding) {
super.configureActionHandlers(binding);
binding.add(MyCustomActionHandler.class);
}
Node GLSP Server
protected override configureActionHandlers(binding: InstanceMultiBinding<ActionHandlerConstructor>): void {
super.configureActionHandlers(binding);
binding.add(MyCustomActionHandler);
}
Request-Response Handling
Action handlers can treat request-response actions in the same way as plain actions. No special handling is required. The action dispatcher tracks all incoming request actions and automatically intercepts the corresponding response action to set the correct response id.
Implementing an Action Handler (GLSP Client)
On the client, GLSP reuses the IActionHandler
API of Sprotty.
Therefore, to create a new action handler, a class that implements the IActionHandler
interface has to be created.
@injectable()
export class MyCustomResponseActionHandler implements IActionHandler {
handle(action: MyCustomResponseAction): void | Action {
// implement your custom logic to handle the action
// Optionally issue a response action
}
}
The handle()
method has to be implemented to provide the custom logic of your action handler.
It optionally returns a response action that should be dispatched after the handler execution.
A dedicated configuration function is available to configure the new action handler in the diagram module (“di.config.ts”):
const diagramModule = new ContainerModule((bind, _unbind, isBound, rebind) => {
const context = { bind, _unbind, isBound, rebind };
configureActionHandler(context, MyCustomResponseAction.KIND, MyCustomResponseActionHandler);
}
The configureActionHandler()
function takes the inversify binding context, the action kind that should be handled, and the action handler class, as input.
It registers the action handler for the given action kind, so that it can be retrieved by the action dispatcher.