Commands

From Developer Documents
Jump to navigation Jump to search

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:

  1. Commands are SCL functions returning a value of type Command a that can be checked and executed
  2. Commands are concepts in ontologies that define different properties containing SCL functions for checking and execution
  3. 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 complicates 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 the 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 a 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 effect 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 checks 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 execution 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.