JSL Service Model

The service layer acts as a front-facing interface masking the underlying entity model. It can hide the complexity and the details of the entity model and provides a simpler interface to the clients of the application. Or it can provide different views for different purposes, allowing changes in functionality without changing the underlying, often rigid data model, which is often created from very different aspects. In addition, the service model also reduces the dependence between the entity model and the outside world.

The main building blocks of the service model are the service access points and the transfer objects.

Transfer object

A transfer object is a container for a set of data fields that is transferred between the service layer and any upper layers (e.g. user interface or external system). Additionally, transfer objects may contain activities such as internal consistency checking, validation and business logic.

The term "transfer object" is a short version of "transfer object type". The correct term would be "transfer object type". However, to distinguish between the type and the instance, the object is called "instance of a transfer object".

To define an transfer object, use the transfer keyword.

Syntax:

transfer [abstract] <name> [extends <transfer>] [mapped <entity> as <alias>] {
    [member] ...
}

where the <name> is the name of the transfer object, and the members are defined between { and }. Possible member definitions are provided in the next section.

Example:

transfer PersonTransfer {
}

The example above defines an empty transfer object named PersonTransfer. This transfer object has no fields or actions.

Field

A transfer object may contain data descriptions called fields. A field has an associated domain type that can be either a primitive or other transfer object, in other words, the field is a typed element. Fields cannot store multiple primitive values (that is, lists, sets), but only a single primitive value. On the other hand, a field can contain a list of transfer object instances to store collection of values.

Use the field keyword to specify a field within a transfer object.

Syntax:

field [required] <primitive> <name>

where <primitive> is the name of a domain model primitive, or

field [required] <transfer>[[]] <name>

where the <transfer> is the name of a transfer object, or

field [required] <union> <name>

where <union> is the name of a union or an in place union. See unions later.

The <name> is the referrable name of the field.

The optional [] indicates that the field is a list of transfer object instances rather than a single reference to one transfer object.

The required keyword means that the value of the field cannot be undefined at the time the object is validated. It does not mean that the field cannot be UNDEFINED, it can. However, if the transfer object is validated it will report fields that are required and having the value of UNDEFINED. See the validate command.

The keyword required is not allowed in fields with list type.

Unlike entities, transfer object fields do not have <default> value expression. To specify the initial value of fields, use the construct action. See object life-cycle later.

Example:

transfer PersonTransfer {
    field required String firstName
    field required String lastName
    field String midName = ""
}

The example above defines a transfer object named PersonTransfer. This transfer object has three fields. firstName and lastName are two required strings, and midName is an optional string with an empty string by default.

Example:

transfer AddressTransfer {
    field required String line1
    field String line2
    field String City
    field String ZipCode
}

transfer PersonTransfer {
    field required AddressTransfer address
}

The second example defines the AddressTransfer transfer object with its fields, and each PersonTransfer instance must have an AddressTransfer.

TODO: private? - for hidden fields, protected? - for read-only fields

Mapping and mapping field

The entity layer and the service layer require to map between their different object models (e.g. between entities and transfer objects). Mapping between entities and transfer objects is a tedious and error-prone task. The mapped transfer objects are intended to facilitate this work. The mapping is a configuration approach that copies entity field values to an instance of a transfer object in a pre-defined way. A transfer object may designate an entity to be the source of the mapping. The transfer object that has a source entity assigned to the mapping is called mapped transfer object.

To define a mapped transfer object use the mapped - as keyword pair.

Example:

transfer PersonTransfer mapped Person as person {
}

The example above defines the transfer object PersonTransfer. This transfer object is mapped to the Person entity. A particular instance of the Person entity is accessible as person within the scope of the PersonTransfer transfer object. We can consider the person as a field of type Person, it is called a mapping field. Although the mapping field can be considered a simple transfer object field, it can be referenced without the self prefix.

The mapping field is not required. In some PersonTransfer instances, the mapping field may refer to a particular Person entity instance, and in others it may be undefined.

Transfer objects that do not have mapping field are unmapped transfer objects.

Derived field

A derived field is a kind of field that provides a flexible mechanism to read its value. The value of a derived field can not set, in other words derived fields are read only. However, derived fields can be used as if they are ordinary fields, but they have a special expressions called getter expression. The getter expression is used to return the value of the derived field. The getter expression must return the same type or same transfer object or same set of transfer objects as the derived field type.

Unlike derived fields of entities, derived fields in transfer objects cannot have arguments.

Use the derived keyword to specify a derived field within a transfer object.

Syntax:

derived <primtive> <name> = <getter>

where <primitive> is the name of a domain model primitive, or

derived <transfer>[[]] <name> = <getter>

where the <transfer> is the name of a transfer object, and <name> is the referable name of the derived field.

The optional [] indicates that the derived field returns a list of transfer object instances rather than a single reference to one transfer object instance.

The <getter> expression returns the value of the derived field whenever it is requested. See expressions later.

Example:

entity Person {
    field required String firstName
    field required String lastName
}

transfer PersonTransfer mapped Person as person {
	derived	String fullName = person.firstName + " " + \
	                          person.lastName
}

The example above defines two stored fields in Person entity. The name of the derived field in PersonTransfer is fullName and its value is calculated by concatenating the firstName and lastName fields of Person mapped entity with a space in the middle. Note the person reference in the getter expression which refers to the person mapping field.

In the getter expressions of transfer objects the self keyword cannot be used.

TBD: self can be used in getter of transfer objects and person mapping field shall be referenced as self.person. This solution allows to refer the transfer object’s own fields in its getter expressions.

transfer SalesPersonTransfer mapped SalesPerson as salesperson { derived LeadTransfer[] leads = salesperson.leads }

transfer LeadTransfer mapped Lead as lead { derived CustomerTransfer customer = lead.customer }

transfer CustomerTransfer mapped Customer as customer { }

Example:

entity SalesPerson {
    field String firstName
    field String lastName

	relation Lead[] leads
}

entity Lead {
	relation required Customer customer
}

entity Customer {
}

transfer SalesPersonTransfer mapped SalesPerson as salesperson {
    // field String firstName
    // field String lastName

    derived String name => salesperson.firstName + " " + salesPerson.lastName
    derived CustomerTransfer[] leads => salesperson.leads

    query CustomerTransfer[] customers() => salesperson.leads.customer
}

The example above defines two derived relations. leads is defined within SalesPersonTransfer and can refer to a list of LeadTransfer transfer objects that belong to a particular salesperson. customer transfer relation is defined within the LeadTransfer and identifies the customer who would make the purchase.

Action

A transfer object can also define behaviors called actions. The action has a body, which is the piece of code (script) that is executed when the action is invoked.

The action may return a computed value to its caller (its return value). This value is often referred to as output. In addition to the output, an action may also return different type of errors. The definition of errors is discussed in chapter Errors.

Actions cannot have input parameters.

Action overriding refers to a subclass redefining the implementation of a action of its parent.

Use the action keyword to specify an action within a transfer object.

Syntax:

action [static] [<type>] <name> [throws <error1> [, <error2>] ...] {
    <script>
}

where the <name> is the name of the action, and the <script> between { and } is the code to execute.

The optional <type> is the output type of the action. If there is no <type> defined, the action cannot return any data. The <type> can be the following:

  • domain primitive

  • single entity or single transfer object

  • list of entities or list of transfer objects

  • union or in place union

The throws keyword is used to declare a list of errors that may occur during the execution of the action code. This optional error list contains the names of the errors that the action may throw and the caller must be prepared to handle them.

The optional static keyword is used to create actions that can be invoked without an existing instance of the transfer object. Static methods cannot use any of the fields or derived fields of the transfer object in which they were defined. In other words, static actions use the transfer objects that contain them as an embedding namespace.

Life-cycle actions

Understanding the concept and behavior of transfer objects is essential when creating, updating, saving, deleting, and working with them.

The lifetime of transfer objects is the time that elapses between the creation and destruction of a particular instance. The events that occur during their lifetime are described by the life-cycle process. There are four important types of events in an object’s life-cycle:

  • Construction

  • Loading

  • Saving

  • Destruction

Construction

The first event that can occur in the life-cycle of a transfer object is the construction.

Once a new, uninitialized transfer object is created, the system calls its construct action. The construct action is a special action that is used to initialize the newly created instance. This action can be used to set initial values for fields. It is not allowed to return any data from a construct action, however it automatically returns the self variable.

A new transfer object is created only when using the new command explicitly. If there is no construct action defined for a transfer object, the new command cannot be invoked for that transfer object type. See commands later.

Use the action and construct keywords to specify a construct action within a transfer object.

TBD: we may omit the action keyword.

Syntax:

action construct [throws <error1> [, <error2>] ...] [{
    <script>
}]

The <script> and the enclosing curly brackets are optional. If they do not exist, the transfer object can be created using the new command, but the fields of the transfer object will be undefined.

The throws keyword is used to declare a list of errors that may occur during the construct action. This optional error list contains the errors that the action may throw and the caller must be prepared to handle them. If the construct action throws an error, the object will not be created.

TODO: special attention must be paid to errors and rollback! TODO: can the rollback be solved in a transaction section of script?

Example:

transfer PersonTransfer {
    field required String firstName
    field required String lastName

    action construct {
        self.firstName = "John"
        self.lastName = "Doe"
    }
}

The example above adds a construct action to the PersonTransfer transfer object. It initializes the firstName and lastName fields after the object has been created.

Destruct

The last event that can occur in the life-cycle of a transfer object is the destruction. At the end of the destruction, the transfer object instance is deleted and will no longer be available. All references to the transfer object instance will be undefined.

Before the transfer object instance is deleted, the system calls the destruct action. The destruct action is a special action that is used to delete mapped entity or invoke other destruct actions. It is not allowed to return any data from a destruct action.

A transfer object instance is deleted when the delete command is invoked. The delete command does not delete automatically all of the transfer object’s own fields. Fields with a transfer object type can be deleted by invoking the delete command in the destruct action.

If there is no destruct action defined for a transfer object, the delete command cannot be invoked for that transfer object type.

Use the action and destruct keywords to specify a destruct action within a transfer object.

Syntax:

action destruct [throws <error1> [, <error2>] ...] {
    <script>
}

The <script> and the enclosing curly brackets are optional. If they do not exist, the transfer object can be deleted, but no action is taken other than deleting the instance.

The throws keyword is used to declare a list of errors that may occur during the destruct action. This optional error list contains the errors that the action may throw and the caller must be prepared to handle them. If the destruct action throws an error, the object will not be deleted.

TODO: due to the recursive manner of destruction, special attention must be paid to errors and rollback!

Example:

transfer AddressTransfer mapped Address as address {
    field required String line1
    field String line2
    field String City
    field String ZipCode

    action destruct {
        delete address  // deletes the address entity
    }
}

transfer PersonTransfer mapped Person as person {
    field required String firstName
    field required String lastName
    field required AddressTransfer address

    action destruct {
        delete self.address  // deletes the address field
        delete person        // deletes the person entity
    }
}

The example above adds a destruct action to the PersonTransfer transfer object. It deletes the mapped Person entity instance.

Loading

TODO: load reads only domain derived primitive fields, derived transfer objects shall be loaded explicitly

Loading can occur any time in the life-cycle of a transfer object. Loading begins with the evaluation of the derived expressions, and the results of the evaluations are placed in the appropriate derived fields.

Once the derived fields of the transfer object instance are set, the system calls the load action. The load action is a special action that is used to set the non-derived fields of the transfer object instance. It is not allowed to return any data from a load action, however it automatically returns the self variable.

Transfer object is loaded when you filter or explicitly use the load command. See commands later. If no load action is defined for a transfer object, the load command can be called, but it will only load the derived fields, if any.

Use the action and load keywords to specify a load action within a transfer object.

Syntax:

action load [throws <error1> [, <error2>] ...] [{
    <script>
}]

The <script> and the enclosing curly brackets are optional. If they do not exist, the transfer object can be loaded, but no action is taken other than loading the derived fields.

The throws keyword is used to declare a list of errors that may occur during the load action. This optional error list contains the errors that the action may throw and the caller must be prepared to handle them. If the load action throws an error, the object will exist in an indeterminate state.

Example:

transfer PersonTransfer mapped Person as person {
    field String fullName

    action load {
        self.fullName = person.firstName + " " + person.lastName
    }
}

The example above adds a load action to the PersonTransfer transfer object. It sets the fullName field by concatenating the firstName and lastName fields of the mapped entity.

Save

Saving can occur any time in the life-cycle of a transfer object.

Transfer object is saved only when using the save command explicitly. Once the save command is invoked, the system calls the save action. The save action is a special action that is used to set the fields of the mapped entity according to the status of the transfer object. It is not allowed to return any data from a save action, however it automatically returns the self variable.

If there is no save action defined for a transfer object, the save command cannot be invoked for that transfer object type. See commands later.

Use the action and save keywords to specify a save action within a transfer object.

Syntax:

action save [throws <error1> [, <error2>] ...] [{
    <script>
}]

The <script> and the enclosing curly brackets are optional. If they do not exist, the transfer object can be saved, but no action is taken at all.

The throws keyword is used to declare a list of errors that may occur during the save action. This optional error list contains the errors that the action may throw and the caller must be prepared to handle them. If the save action throws an error, the object will exist in an indeterminate state.

Example:

transfer PersonTransfer mapped Person as person {
    field String firstName
    field String lastName

    action save {
        person.firstName = self.firstName
        person.lastName = self.lastName
    }
}

The example above adds a save action to the PersonTransfer transfer object. It sets sets the firsName and lastName fields of the mapped entity field in accordance with the fields of the transfer object respectively.

TODO: special attention must be paid to errors and rollback!

Inheritance

Inheritance is a mechanism by which more specific transfer objects incorporate structure of a more general transfer object (called parent transfer object).

Transfer objects may inherit fields, derived fields, mapping field, actions and constraints from their parent transfer object. A transfer object and its parent transfer object are in IS-A relation, so a tranfer object can appear anywhere in the role of its parent transfer object.

Inherited members of a transfer object, which were defined in the parent behave as if they were defined in the transfer object itself.

A transfer object may be the parent of any number of other transfer objects, but it can have only one parent at most. In other words, the multiple inheritance is not supported between tranfer objects.

A transfer object should not be inherited from itself, either directly or indirectly.

A transfer object may override inherited actions, other inherited members (fields, derived fields and mapping field) cannot be overridden with the same name. It is also not allowed to override the mapping field in transfer objects that inherit a mapping field.

Example:

transfer IdentifiableTransfer {
    field required email
}

transfer PersonTransfer extends IdentifiableTransfer mapped Person as person {
    derived String firstName = person.firstName
    derived String lastName = person.lastName
}

transfer SalesPersonTransfer extends PersonTransfer {
    derived PersonTransfer manager = person!asType(SalesPerson).manager
}

In the above example the PersonTransfer inherits the required email field of the IdentifiableTransfer and defines two more derived fields.

The SalesPersonTransfer inherits both email field and the derived fields and, in addition, defines a relation to its manager. Note that mapping is not enabled in SalesPersonTransfer. Thus, the person mapping field is casted (using asType()) to SalesPerson entity before its manager relation is accessed.

TBD: mapped entity can be narrowed in children.

Override

A transfer object may override inherited actions, including life-cycle actions. Overriding is a mechanism that enables a transfer object to provide different implementation for an action that is already defined in its parent transfer object.

Use the override keyword to override a specific action within a transfer object.

Syntax:

override <name> {
    <script>
}

where the <name> is the name of the action that will have the new <script> implementation.

With the override mechanism the transfer object replaces the implementation of an action that has the same name in the parent transfer object. The <script> of the action that is executed will be determined by the transfer object instance that is used to invoke it. If an instance of a parent transfer object is used to invoke the method, then the <script> in the parent transfer object will be executed, but if an instance of the child transfer object is used to invoke the method, then the <script> in the child transfer object will be executed. In other words, it is the type of the transfer object instance being referred to (not the type of the field or variable) that determines which version of an overridden action will be executed.

Example:

transfer PersonTransfer {
    field required String firstName
    field required String lastName

    action String getLabel throws GenericError {
        return self.firstName + " " + self.lastName
    }
}

entity SalesPersonTransfer extends PersonTransfer {
    override getLabel {
        return self.firstName + " " + self.lastName + " (sales representative)"
    }
}

In the example above the SalesPersonTransfer overrides the implementation of the getLabel method defined in PersonTransfer. Note that neither the return value type nor the error list is redefined in the override.

Union

A union is a special type to hold different type of transfer objects. A union must have at least two members, but only one member can contain a value at any given time.

Unions cannot be organized into a list.

Unions can be used as transfer object field types or action return types.

Use the union keyword to specify a union.

Syntax:

union <name> { <member1> | <member2> [| <member3>] ... }

Example:

transfer SalesPersonTransfer {
}

transfer DeveloperTransfer {
}

union Employee { SalesPersonTransfer | DeveloperTransfer }

The example above defines the Employee union, which has two members. At any point of time an Employee union may hold exactly one SalesPersonTransfer or exactly one DeveloperTransfer or none of them.

In place union

Unions can be defined in the place of their use. In place unions do not have name.

Example:

transfer SalesPersonTransfer {
}

transfer DeveloperTransfer {
}

transfer SalaryTransfer {
    field Integer amount
    field required { SalesPersonTransfer | DeveloperTransfer } employee
}

In the example above, the employee relation of SalaryTransfer must refer to a SalesPersonTransfer or a DeveloperTransfer transfer object.

Constraint

A constraint represents some restriction related to a transfer object. A constraint is specified by a logical expression which must evaluate to a true or false. Constraint must be satisfied (i.e. evaluated to true) by a correct use of the system.

One transfer object may contain multiple constraints that must be satisfied. The order in which the multiple constraints are evaluated is the same as the order in which the constraints in the jsl file are declared.

Use the constraint keyword to specify a restriction on a transfer object. The syntax of constraint is the same as it is defined at entities.

Example:

error NameIsTooShort {
    field required String name
}

transfer Person {
    field required String firstName
    field required String lastName
    field String midName = ""

    constraint self.firstName!length() + self.lastName!length() > 4 \
    onerror NameIsTooShort(name = self.firstName + " " + self.lastName)
}

Constraints defined in transfer objects are not evaluated automatically. To check a transfer object instance against its constraints, use the validate command. See commands later.