Commands
Requirements
The core idea of Simantics command system is to have all user interface operations go through SCL function calls. This ensures they are all available in SCL for writing scripts and regression tests. The direct user need driving the development is recording of the commands for later replaying.
Because recording and scripting are the main applications, the commands are defined to be operations that make modifications to the persistent state of the application without any user interaction. Thus for example a function that opens a wizard is not a command, but the modification to a model that is executed when the wizard is finished is a command. There are probably cases where the difference is not so clear (is changing of profiles a command?).
For command recording, each command must have a context where it is executed. Particularly when making modifications to two models in the same workspace, the commands should be recorded to two separate command logs. This may cause problems, when the command affects the existence of the model (creating new model, importing model, removing model), the command affects multiple models (moving stuff between models) or the command causes a persistent effect outside of models (changing preferences, importing ontologies).
The command system should specify how commands are executed from Java code, how they are referred in modelled user inteface actions (such as modelled context view or selection view) and how new commands are contributed.
The main challenge of the system is the difference between two modes of execution. When writing a script, the state of the application at execution time is unknown and all commands should be available. Each command is executed atomically and they either succeed or fail with an error message that is either written to the console, if the execution is initiated there, or otherwise shown in a dialog. On the other hand, when executing commands from UI, the user expects that UI hides or disables the actions that are not allowed in the current operating context or will surely fail.
The second challenge is the serialization of the commands for recording so that commands would be valid with maximal probability when replaying in an application context with possible minor differences.
Design
Introduction
The most important thing of the command is the modification it makes to the model. However, for use in UI, some additional information is needed such as which roles are allowed to execute the command and a function to check is the command execution possible.
The following approaches were consided for bundling this information together:
- Commands are SCL functions returning a value of type
Command a
that can be checked and executed - Commands are concepts in ontologies that define different properties containing SCL functions for checking and execution
- Commands are specified as separate SCL functions that are tied together by naming conventions
The first approach has some drawbacks. First, it is not possible to know the allowed roles
without first giving all parameters to the function. Second, without adding some new syntactic
sugar, the Command
-type complicated writing of scripts. Either every command
has to be wrapped like
exec (newDiagram ...)
or written in monadic style. A benefit of Command
-type might be the ability to create
new commands by combinators.
The second approach makes possible to define arbitrary properties for the commands, so it is extensible. Its drawback is need for a read transaction for obtaining information about commands. It makes it also heavier to define new commands. A command contribution might involve additions in three places: Java code implementing the operation, SCL code importing the Java code and ontology referring to SCL.
The third approach has the flexibility of the ontology approach, but is lighterweight. Role and checking may have some defaults (allowed for any role, no checking), which makes it possible to consider any SCL function as a command. For these benefits we choose this approach.
Defining commands
Commands are defined as ordinary SCL-functions. The name of the command (and thus the defining function) should be in imperative mood (createDiagram, not newDiagram). The command is executed in a write transaction, thus WriteGraph and Proc effects are allowed in the signature.
Additional SCL-functions command_check
and command_role
specify the related
checking function and role. Although we follow camel case naming convention in SCL, in this case
the suffixes are separated by _
. Role function should return the role name (string).
The parameters of the role function are some prefix of the parameters of the command, typically
role function does not have parameters at all.
The signature of the checking function is related to the signature of the command in more complicated
way in order to make checking of partial parameters possible. Basically some suffix of the signature
is replaced by Boolean
and any right subtype may be enclosed in Maybe
.
The checking function may have ReadGraph event and if it has the command system ensures that it is
executed in the read transaction.
So, if the signature of the command is
A -> B -> <WriteGraph> C
then, the signature of the checking function can be for example:
Boolean A -> <ReadGraph> Boolean A -> B -> <ReadGraph> Boolean A -> <ReadGraph> Maybe (B -> Boolean) A -> <ReadGraph> Maybe (B -> <ReadGraph> Boolean)
If the final Boolean value is false or any of the intermediate Maybe value is Nothing, then the command may not be executed.
Here is an example:
setPropertyAsString :: Variable -> String -> <WriteGraph> () setPropertyAsString_check :: Variable -> <ReadGraph> Maybe (String -> <ReadGraph> Boolean) setPropertyAsString_role :: String
The command takes a variable and a value in a string form as a parameter. The checking function first check, if the variable is writable. The second part of the checking tests that the string value is valid.
Java API
Java API for executing commands is meant to be used from during execution of
UI operations. Commands are executed in contexts (usually models) that
must be first obtained. New command can be created with newCommand
method in the context. Parameter name
is a full path of the
SCL-function implementing the command.
public class CommandContext { public static CommandContext getContext(Resource resource); public static CommandContext getContext(RequestProcessor graph, Resource resource); public Command newCommand(String name); }
Command interface has methods for adding and removing parameters, checking that command is eligible for exectution and committing the command. Even if checking accepts the parameter, the command may fail in commit. On failure, an exception is thrown (currently no special exception is specified).
public interface Command { Command addParameters(Object ... parameters); Command removeParameters(int count); boolean check(); boolean check(RequestProcessor graph); void commit(); void commit(RequestProcessor graph); }
(TODO async methods?)
The interface handles roles automatically. When the current user has no privileges to execute the command, check method always fails.
Example (fictional):
public Runnable renameContribution(Resource target) { final Command command = CommandContext.getContext(target) .newCommand("Simantics/FictionalModule/rename", target); if(!command.check()) return null; // Cannot rename the target else return new Runnable() { public void run() { new RenameWidget(command).start(); } }; } public class RenameWidget { Command command; public RenameWidget(Command command) { this.command = command; } public boolean validate(String name) { boolean result = command.addParameters(name).check(); command.removeParameters(1); return result; } public void finish() { try { command.addParameters(getName()).commit(); } catch(Exception e) { show error to user } } }
Recording
When a command is executed, the textual form of the command is written to the metadata of the change set. The command metadata is associated with a certain context (usually a model). Although the metadata is enough to reconstruct the whole command history, a snapshot of the history is periodically written to a separate file to save information in the case the whole database corrupts.
The writing of command metadata happens in the commit-method of the Command interface
after the change is successfully made the the database and no exception is
thrown. Each command is written in a form readily suitable for execution in a script
with two exceptions: Command begins with the full name of the command function so that
no imports are not expected to be available. It is assumed that a resource
named context
is defined.
Command parameters are serialized with a type class:
class RepresentableParameter a where representParameter :: Resource -> a -> <ReadGraph> String
The first parameter is the current context resource. All references are made relative
to that resource that commands can later executed in some other context. The method
representParameter
works much like show
in Prelude, but
is also able to write expressions for finding resources in ontologies and under
the context resource.