Business Logic

When interacting with the REST API it is oftentimes needed to have an application-level layer of business logic in place that represents the invocation point for the API. It can be used for orchestration, validation, or other tasks that should happen when the API client interacts with the system. The Entities API does not allow placing additional orchestration business logic when interacting with the API. Instead, the API client directly interacts with the Data Access Layer of Jmix.

For exposing business logic to the API client, Jmix provides two ways:

  1. Services API

  2. Custom controllers

In the next section, we will take a look at both options to understand where the difference between those approaches is.

Services API

Let’s look at the first approach to expose business logic to the API client: the Services API.

The Services API allows exposing an arbitrary Spring bean as an HTTP endpoint. In this case, Jmix will take care of the HTTP interactions like providing HTTP response codes, perform error handling, etc.

In this overview you can see the interaction between the API client and the Jmix application when using the Services API:

business logic diagram

Exposing a Service

To use a Spring bean as part of the Jmix Services API, it needs to meet one of the following conditions:

  • New Experimental Annotation-Based Approach: The Spring bean must be created using the new experimental annotation-based method.

  • Traditional Approach: The Spring bean must fulfill these criteria:

    1. The Spring bean needs to be annotated with Spring’s @Service annotation (a specialized version of the @Component annotation).

    2. The Spring bean needs to be registered in the rest-services.xml configuration file.

Let’s examine these two methods in more detail.

Using Annotations

This is a part of experimental API which may be changed or removed in the future. Use at your own risk.

Create the Spring bean and annotate it with the @RestService annotation.

import io.jmix.rest.annotation.RestMethod;
import io.jmix.rest.annotation.RestService;

import java.math.BigDecimal;

@RestService("sample_OrderService") (1)
public class OrderService {

    @RestMethod (2)
    public BigDecimal calculateTotalAmount(int orderId) {
        // ...
    }
}
1 The @RestService annotation is used to mark a service class that should be accessible through a REST API.
2 The @RestMethod annotation is used to configure the mapping between a service method and a specific REST endpoint. You can pass the httpMethods parameter, which accepts a list of possible HTTP methods for invoking services through the Generic REST API. By default, it includes GET and POST.

Using rest-services.xml

Let’s look at the first part with this example:

CalculationServiceBean.java
import org.springframework.stereotype.Service;

@Service("sample_OrderService") (1)
public class OrderService {

    public BigDecimal calculateTotalAmount(int orderId) {
        // ...
    }
}
1 The OrderServiceBean is registered as the Spring @Service with the name sample_OrderService.
If the service name is not specified explicitly in the annotation, it is assumed to be equal to the simple class name with the first letter in lowercase. In the example above it would be orderService.

The second part is to mention all Service methods as API endpoints. This happens via an XML configuration file, normally called rest-services.xml. You need to create this new file in your Jmix application as part of src/main/resources. It lists all service methods with information about the parameters that you want to expose.

rest-services.xml
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://jmix.io/schema/rest/services">
    <service name="sample_OrderService"> (1)
        <method name="calculateTotalAmount"> (2)
            <param name="orderId"/> (3)
        </method>
    </service>
</services>
1 You register the Service by its Spring component name.
2 Each method that you want to expose needs to be mentioned here.
3 The parameter needs to be described by its name and optionally by its type.

After the file has been created, and the services have been defined, the last part is to register the rest-services.xml configuration in the application.properties of your Jmix application:

application.properties
jmix.rest.services-config = rest-services.xml
The value of the serviceConfig is a reference to the file within the classpath. In this case, the file is located directly in the classpath root under src/main/resources. If you for example put the file in the src/resources/com/example/rest-services.xml package, the value would be: com/example/rest-services.xml.

Using the Services API

Once we have exposed the Service through the Services API, you can invoke it from an API client. This is possible by HTTP GET or HTTP POST.

Invoke a Service via GET

In the case of HTTP GET, you need to provide the method parameter values as URL query parameters:

Calculate Total Order Amount via HTTP GET
GET http://localhost:8080/rest
            /services
            /sample_OrderService
            /calculateTotalAmount?orderId=123
Authorization: Bearer {{access_token}}
Response: 200 - OK
450.0
When using GET for invoking a service through the Services API, the OAuth access token still needs to be provided by the HTTP Authorization header. It is not possible to append the access token as a URL query parameter.

A service method may return a result of a simple data type, an entity, an entity collection, or a serializable POJO. In our case, the service method returns an int, so the response body contains just a number.

Invoke a Service via POST

Alternatively, it is also possible to invoke the Service via HTTP POST. This is in particularly useful, when the Service method has one of the following parameter types:

  • Entities

  • Entity Collections

  • Serializable POJOs

Suppose we added a new method to the OrderService created in the previous part:

OrderServiceBean.java
@Service("sales_OrderService")
public class OrderService {

    public OrderValidationResult validateOrder(Order order, Date validationDate){
        OrderValidationResult result = new OrderValidationResult();
        result.setSuccess(false);
        result.setErrorMessage("Validation of order " + order.getNumber() + " failed. validationDate parameter is: " + validationDate);
        return result;
    }
}

With the following structure for the OrderValidationResult POJO as the result object:

OrderValidationResult.java
import java.io.Serializable;

public class OrderValidationResult implements Serializable {

    private boolean success;

    private String errorMessage;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

The new method has an Order entity in the arguments list and returns a POJO. Before the invocation of the REST API, the new method also must be registered in the rest-services.xml. Once you exposed the method you can perform the API call:

Invoke Order Validation via HTTP POST
POST http://localhost:8080/rest/services/sales_OrderService/validateOrder

{
  "order" : {
    "number": "00050",
    "date" : "2016-01-01"
  },
  "validationDate": "2016-10-01"
}

The REST API method returns a serialized OrderValidationResult POJO:

Response: 200 - OK
{
  "success": false,
  "errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}

Passing Parameters

Parameter values must be passed in a format defined for the corresponding datatype.

  • If the parameter type is java.util.Date, then the value is handled by DateTimeDatatype. This datatype implementation uses the ISO_DATE_TIME format where the date and time parts are separated with T, for example 2011-12-03T10:15:30.

  • Parameters of java.sql.Date type are handled by DateDatatype which uses ISO_DATE format, for example 2011-12-03.

  • Parameters of java.sql.Time type are handled by TimeDatatype which uses ISO_TIME format, for example 10:15:30.

Custom Controller

The second way of exposing business logic as an API is the ability to use custom HTTP controllers. The main difference is that in this case, it is also possible to influence the HTTP interactions (like status codes, security, etc.) on your own. Jmix uses the default mechanisms from Spring MVC for creating HTTP endpoints.

Use-cases for custom controllers could be:

  • explicitly define HTTP status codes

  • use other request & response content type than JSON

  • set custom response headers (e.g. for caching)

  • create custom error messages from exceptions

In these situations, the generic Services API might be not flexible enough to accomplish your goals. Therefore Jmix allows natively integrate Spring MVC controllers into a Jmix application.

Creating Custom Controllers

To create a Spring MVC controller, it is only required to create a Spring bean in the Jmix application annotated as a Spring MVC controller. Jmix itself does not have any further requirements over Spring MVC. Let’s look at an example Controller:

OrderController.java
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController (1)
@RequestMapping("/orders")  (2)
public class OrderController {
    // ...
}
1 The custom controller is marked as @RestController to indicate to Spring that this bean contains HTTP endpoints.
2 The request mapping defines the base path for this Controller.

Now that the Spring controller is registered, we can create a method exposing a particular HTTP endpoint with it:

OrderController.java
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RestController
@RequestMapping("/orders")
public class OrderController {

    @GetMapping("/calculateTotalAmount") (1)
    public ResponseEntity<OrderTotalAmount> calculateTotalAmount(
            @RequestParam int orderId  (2)
    ) {

        BigDecimal totalAmount = orderService.calculateTotalAmount(orderId);

        return ResponseEntity (3)
                .status(HttpStatus.OK)
                .header(HttpHeaders.CACHE_CONTROL, "max-age=31536000")
                .body(new OrderTotalAmount(totalAmount, orderId));

    }
}
1 The method calculateTotalAmount is annotated with @GetMapping indicating that it is accessible via HTTP GET on the subpath /calculateTotalAmount.
2 The parameter orderId is retrieved via URL query parameters.
3 We can use Spring’s ResponseEntity class to indicate a JSON response together with different HTTP aspects.

More detailed information on the various aspects of how to create Spring MVC controllers can be found in the Spring guide: Building a RESTful Web Service as well as the reference documentation for Spring MVC.

With that controller in place, Jmix can serve this HTTP endpoint. Let’s have a look at how to interact with the controller:

Invoke Custom Orders Controller
GET http://localhost:8080/orders/calculateTotalAmount?orderId=123

The result contains the calculation result exposed as JSON as well as the defined HTTP headers:

Response: 200 - OK
HTTP/1.1 200
Cache-Control: max-age=31536000
Content-Type: application/json

{
  "orderId": 123,
  "totalAmount": 450.0
}

Securing Custom Controllers

To secure a custom controller via the same OAuth2 mechanism that the other parts of the Jmix REST APIs use, register the controller’s URL pattern in the jmix.rest.authenticated-url-patterns application property:

application.properties
jmix.rest.authenticated-url-patterns = /orders/**

Here, the /orders/** wildcard indicates to Jmix that all endpoints that start with /orders/ should also use the OAuth2 mechanism.

The value can contain a comma-separated list of Apache Ant style URL patterns.

Trying to invoke the Order Controller now without a valid OAuth2 token will result in an HTTP 401 - Unauthorized:

Response: 401 - Unauthorized
HTTP/1.1 401
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"

{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

Authenticated endpoints can rely on data access control provided by the Jmix security subsystem. If your controller uses DataManager to load or save data, it will check rights of the authenticated user on entity operations. In the following example, the "Access denied" exception will be thrown if the user has no rights to read the Order entity:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private DataManager dataManager;

    @GetMapping("/all")
    public List<Order> loadAll() {
        return dataManager.load(Order.class).all().list();
    }

If you want to limit access also to entity attributes, use the EntitySerialization bean for serializing entities returned from the endpoint. In the following example, only attributes, permitted by the entity attribute policy will be returned in JSON to the client:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private DataManager dataManager;
    @Autowired
    private EntitySerialization entitySerialization;

    @GetMapping("/all")
    public String loadAll() {
        List<Order> orders = dataManager.load(Order.class).all().list();
        return entitySerialization.toJson(
                orders,
                null,
                EntitySerializationOption.DO_NOT_SERIALIZE_DENIED_PROPERTY
        );
    }