Understanding User-Defined Routine Statements

Tasks can rely on the Backend Service to execute user-defined routines.

The ExecuteRoutineStatement.of(Expression routineName) method creates a statement that, when executed, executes the given routine on the Backend Service.

The routine name is provided as an expression that evaluates to a string, which is then matched against the name provided to the @Routine annotation on the Java class that implements the routine.

@Bean
public ScriptedTaskDescription myFirstTask() {
    ScriptedTaskDescription task = new ScriptedTaskDescription("Task 1", "My first task");

    task.initI18nNameAndDescription("TASK_1");

    VariableAccessExpression scenarioData = VariableAccessExpression.of("scenarioData");
    VariableAccessExpression textOutput = VariableAccessExpression.of("textOutput");

    task.getScript()
        .addStatement(AskInputStatement.of(scenarioData.getVariableName(), true, scenarioId(WRITABLE), "Select a scenario to edit"))
        .addStatement(ExecuteRoutineStatement
            .of(StringExpression.of("SampleEditScenario"))
            .withInput(ScenarioDataExpression.of(scenarioData))
            .withOutput("message", textOutput.getVariableName()))
        .addStatement(SetTaskOutputStatement.of("my_task_output", textOutput));
    
    return task;
}

A user-defined routine is a class of the Backend Service annotated with @Routine(...). Annotating the routine class with @Routine automatically makes it a Spring component, the bean name of this component is the same as the routine name.

Here is an example of routine:

import ...

@Routine
public class SampleEditScenario {

    private static final Logger LOGGER = LoggerFactory.getLogger(SampleEditScenario.class);
    private final Random random = new Random();

    // Set execution parameters
    public void execute(RoutineExecutionContext executionContext, ScenarioData scenarioData) throws RoutineExecutionException {
        LOGGER.info("Routine started by {} with job {}" context.getExecutingUserId(), context.getJobId());
        CapacityPlanning collector = executionContext.getCollector(scenarioData);

        populate(collector);

        // Save only the modified entities
        executionContext.markModified(scenarioData, collector, "Country", "Plant");

        // Set return values
        executionContext.emit("message", "Scenario edited!");

    }

    /**
     * Populates the collector with a new country, plant.
     * @param collector the collector to populate
     */
    private void populate(CapacityPlanning collector) {
        Country country = collector.createCountry();
        country.setId(String.valueOf(random.nextInt(1000)));
        country.setName("Country " +  random.nextInt(1000));

        Plant plant = collector.createPlant();
        plant.setPlantId("Plant " + random.nextInt(1000));
        plant.setCountry(country);

    }
}

Additional information can be given for the execution of the routine with the following methods.

  • withInput(Expression value) provides a value for the next argument of the execute() method. See below details on the signature of this method.

  • withOutput(String outputName, String variableName) will store the value of the specified routine output into the script memory under the specified variable name.

These methods are to be used for all types of routine inputs and outputs (within the supported types, see below). They will typically be repeated as many times as needed to address all the inputs and outputs of the routine.

This class must provide an execute() method, the signature of which is discussed below.

    // Set execution parameters
    public void execute(RoutineExecutionContext executionContext, ScenarioData scenarioData) throws RoutineExecutionException {
        LOGGER.info("Routine started by {} with job {}" context.getExecutingUserId(), context.getJobId());
        CapacityPlanning collector = executionContext.getCollector(scenarioData);

As a routine is implemented by a class in the Backend Service, this class must have an execute() method.

The first argument of this method must be the RoutineExecutionContext described below. The remaining arguments must match, in number and in type, the calls to the ExecuteRoutineStatement.withInput() method in the task description. The type of an argument is determined as follows, depending on the expression type of the corresponding input.

  • An argument for a Boolean input should be typed with java.lang.Boolean or boolean.

  • An argument for a numeric input should be typed with java.lang.Number.

  • An argument for a text input should be typed with java.lang.String.

  • An argument for a blob input should be typed with byte[].

  • An argument for a file input should be typed with FileValue.

  • An argument for a temporal input should be typed with java.time.temporal.Temporal.

  • An argument for a scenario data input should be typed with ScenarioData.

  • An argument for a list input should be type with java.util.Collection<T>, where T is the type corresponding to the elements of the list, for example String if the list contains text elements.

The execute() method may have any return type, including void. If it returns an int, then this return value is used as the exit code of the ExecuteRoutineStatement statement. Otherwise, the statement has exit code zero.

As mentioned, the first argument of the execute() method is an execution context. It has the following methods.

  • getExecutingUserId() returns the ID of the user who has triggered the execution of the job. It can be set as follows:

            LOGGER.info("Routine started by {} with job {}" context.getExecutingUserId(), context.getJobId());
  • getJobId() returns the ID of the job that requested the execution of the current routine. It can be set as follows:

            LOGGER.info("Routine started by {} with job {}" context.getExecutingUserId(), context.getJobId());
  • getCollector(ScenarioData scenarioData) turns a scenario data object, such as the one passed in an argument that corresponds to a call of withInput() on a scenario data expression, into an instance of the collector class of your application. It can be set as follows:

            CapacityPlanning collector = executionContext.getCollector(scenarioData);
  • markModified(ScenarioData scenarioData, DbDomCollector collector, String... modifiedTables) records modifications made to the scenario data and saves them in a collector, optionally restricting the modifications to a list of entities. It can be set as follows:

            executionContext.markModified(scenarioData, collector, "Country", "Plant");

    The markModified(ScenarioData scenarioData, DbDomCollector collector, boolean checkSchema, String... modifiedTables) variant has an additional Boolean argument to run the schema checkers on the scenario. In a CDM application, if no entity is indicated, it records the modifications on all visible scenario types of the declared scenario.

  • emit(String outputName, Object outputValue) sets the value of the routine output with the specified name. It can be set as follows:

            executionContext.emit("message", "Scenario edited!");