LSPS documentation logo
LSPS Documentation
Working with a Model

At some point you will want to work with data of model instances from your application. When manipulating a model instance, make sure to take into consideration:

Execution Levels

An execution context can exist on different execution levels so one context element can have different values in context versions on different levels.

This mechanism serves to separate possibly transient data from the "real" data: the real data exist in the contexts on the base level or 0 level, the execution level where model instances are created. Consequently, for example, changes on shared records in contexts on this level are reflected instantly in the database.

From a custom object, you can create contexts on sublevels: on the sublevel context you work with copies of the contexts and their data. Once happy with the changes, you can merge your changes into its super contexts. Sublevels are designated with an additional digit added to the original level digit separated by a colon, for example 0:1, 0:1:1, 0:1:2 etc.

Note: The GUI mechanism provided by the ui module makes use of execution-level mechanism:

  • Each form is created on the so-called screen level
  • Contexts of View Models are created on sublevels referred to as evaluation levels.

Restrictions

  • A shared record instance created in a non-base level and not merged into the base level is not registered in the entity manager and hence not returned by queries.
  • Functions with side effects can cause changes in application state even if evaluated in a non-base evaluation level. Such functions are, for example, createModelInstance() and sendSignal().

Creating an Execution Level

To create an execution level, call com.whitestein.lsps.engine.state.xml.EvaluationLevelUtils.nextSublevel(String level, ModelInstance modelInstance).

Note that the level does not contain any data when created: the data of non-base levels are loaded when you request an entity that is not present on that level. The system attempts to load it from the immediate parent context and, if not available, it continues to request higher context levels until it reaches the base level. It is when you change the context data that a context on a non-base level stores data.

Merging an Execution Level

To merge all changes from a non-base context into the context on the parent level, use the com.whitestein.lsps.engine.lang.EvaluationLevelMerger.mergeLevel(String level).

On merge, the system checks for data conflicts. For example, if a variable is changed in two contexts on the second level (the one above the base level) and both context are merged to the base-level context, a conflict is detected. In the case of records, the conflict check is performed on each property: If the property P1 of a record R is changed in one context and property P2 of the same record R is changed in another context no conflict is detected during merge.

Cleaning an Execution Level

To clean changes in a level, call one of the com.whitestein.lsps.engine.state.xml.EvaluationLevelUtils methods:

  • cleanDataOfEvaluationLevel(ModelInstance modelInstance, String level)
  • cleanDataOfLevelAndSublevels(ModelInstance, String).

Note that the cleanDataOfLevelAndSublevels(ModelInstance, String) method cleans changes in the given level and in all child levels.

  • To remove a context from a level, use one of the methods:
    • removeDataOfEvaluationLevel(ModelInstance, String level)
    • removeDataFromLevelAndSublevels(ModelInstance, String level): removes all entities that belong to the execution level and its sub-levels

Checking for Changes on an Execution Level

To check if there are changes in the non-base levels, call com.whitestein.lsps.engine.state.xml.ModelInstance.isDirty().

Creating a Record

To create a record instance in the execution context of your custom object, use the createRecord() method of the namespace.

//custom form component implementation (the class extends com.whitestein.lsps.vaadin.forms.FormComponent):
RecordHolder colorHolder = getNamespace().createRecord("forms::Color");
//custom task and function:
factory.createRecord(
  "GoogleCalendar::GoogleCalendarEvent",
  new HashMap<String, Object>() {
    { put("title", event.getTitle().getPlainText());
      put("content", event.getTextContent().getContent().getPlainText());
      put("date", new Date(event.getTimes().get(0).getStartTime().getValue()));
    }
  }
);

Generating Classes and Interfaces for Records

To work more efficiently with record instances and their types in the LSPS Application, generate the Java wrapper classes for the types and use these classes rather than using the RecordHolder class to work with Records.

Example of difference in coding

// original access via context:
RecordHolder record = ctx.getNamespace().createRecord("core::ConstraintViolation");
record.setProperty("message", msg);
 
// better with generated Java class for records:
ConstraintViolationRecord better = new ConstraintViolationRecord(record);
better.setMessage(msg);

When you generate Java wrappers for records, the system creates the following:

  • subpackage for each module
  • class for records in their respective subpackage
  • interface for record classes
  • RecordWrapperFactoryImpl for individual modules

To generate Java classes and interfaces for records, do the following:

  1. Select the record and configure the properties of the generation:
    1. Open the Java tab in the Properties view.
    2. To generate a class, select the Generate Java class option and the target Java class name.
    3. Optionally, set the Java record type constant name that will hold the path to the record.
  2. Open the Properties view of individual record fields and on the Java tab, set the properties of the methods generated for the field.
  3. Right-click the module or project with the records and go to Generate > Record Java Sources and, in the dialog, define the export properties:

    • Source folder: target src folder in a Java project
    • Package: target package name

    The Java sources are generated into a subpage with the name of the module.

    • Class name prefix: prefix of exported class names
    • Class name suffix: suffix of exported class names
    • Class extends superclass: superclass of the generated record classes
    • Class implements interface: interface of the generated record classes
    • Generate interfaces: select to generate also interface for the record classes
    • Additional record types: other records that should be included in the operation

      The parameter is primarily intended for inclusion of library records.

    • Selected modules: the system generates Java classes for the selected Modules and any dependencies
      generatingJavaRecordHolders.png
      Generating Java record wrappers
  4. Refresh the project with the target src directory.
generatedJavaRecordHolders.png
Generated Java record wrappers
Implementation of a custom function and its definition

customFunctionWithRecordHolder.png
Custom Function implementation and definition

Checking a Record Constraint

To work with constraints on a record, use the following methods:

  • getConstraints returns a collection of all constraints that are applied to a given record type and properties.These can be potentially null.
    executionContext.getProcessModel().getConstraints(recordA, propertyA)
    
  • findTag returns a validation tag with the given qualified name (may be simple or * full).
    executionContext.getProcessModel().findTag(qid)
    

Throwing a Signal

A Signal is a special object used for communication within a model instance or with another model instance.

You can produce signals in your custom object using the server API. To catch signals, use the Signal Start Events or Catch Signal Intermediate Events.

Note: You can also use signal modeling elements to work with signals in your models and signal-related standard-library functions, such as sendSignal().

You can work with signals as follows:

  • To send a synchronous signal to the parent model instance of the custom object, use the addSignal() call of the object's context. Sending a signal to the current model instance from a custom task type
    //start method of a custom task
      @Override
      public Result start(TaskContext context) throws ErrorException {
        Decimal d = (Decimal) context.getParameter("number");
        Integer i = d.intValueExact();
        //adding the signal to the task type context:
        context.addSignal(
           "prime check output:"
           + System.out.println(org.apache.commons.math3.primes.Primes.isPrime(i))
        );
        return Result.FINISHED;
      }
    
  • To send a signal to another model instance:
    1. Define and register your object as an EJB.
    2. Inject CommunicationService into your bean object.
    3. Send a signal with the sendSync() or sendAsynch() method of the CommunicationService bean.

      Example sendSync() call from the start method of a custom task type

      @EJB
        private CommunicationService communicationService;
        ...
        @Override
        public Result start(TaskContext context) throws ErrorException {
          long modelInstanceId = 71000;
          String signal = "signal";
          SignalMessage signalMessage = new SignalMessage(
              //sender of the signal:
              Identifier.ofModelInstance(context.getModelInstance().getId()),
              //receiver of the signal:
              Identifier.ofModelInstance(71000), new ObjectValue(signal));
          try {
            //sending the signal:
            communicationService.sendSync(signalMessage);
          } catch (ModelInstanceNotFoundException | InvalidModelInstanceStateException e) {
            e.printStackTrace();
          }
          return Result.FINISHED;
        }
      

Throwing an Error

You can throw errors directly from your custom objects.

To catch error, use the Error Start Events or Error Intermediate Events.

To throw an error, do the following:

  • In implementation in the Expression Language, use the error(<error_code>) function.
    errorInFunctionInEL.png
  • In implementation in Java, throw com.whitestein.lsps.common.ErrorException.
    errorInFunctionInJava.png

Creating Hooks on Model Execution

To perform an action always when an instance of a model is created, started, or finished, do the following:

  1. Create a stateless bean that implements the ModelInstancePlugin interface.
       public class MyModelInstancePlugin implements ModelInstancePlugin {
            
          @Override
          public void onCreate(CreateCommand cmd, ModelInstanceEntity modelInstance) {
            System.out.println(">>>>>>>>>>>>>>>>>> This is on create hook <<<<<<<<<<<<<<<<");
          }
        ...
    
  2. Register the bean with the ComponentServiceBean:
    @EJB(beanName = "MyModelInstancePlugin")
    private ModelInstancePlugin myMIPlugin;
      
    @Override
    protected void registerCustomComponents() {
      register(myMIPlugin, ModelInstancePlugin.class);
    }
    

Invoking the Command-Line Console

To use the command-line tool from your application, do the following:

  1. Include the mconsolecl dependency in your pom.xml:
    <dependency>
        <groupId>com.whitestein.lsps.mconsolecl</groupId>
        <artifactId>lsps-mconsole-cl</artifactId>
        <version>${project.version}</version>
    </dependency>
    
  2. Call the main method with the cli command as its String[] argument.
    com.whitestein.lsps.mconsolecl.Main.main(new String[]{"arg 1", "arg 2"});
    

For details about the console are available in the Management documentation.