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:
-
Services API
-
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:
Exposing a Service
To use a Spring bean as part of the Jmix Services API, it needs to fulfill the following criteria:
-
The Spring bean needs to be annotated with Spring’s
@Service
annotation (a specialized version of the@Component
annotation). -
The Spring bean needs to be registered in the
rest-services.xml
configuration file.
Let’s look at the first part with this example:
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 specfied 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.
<?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:
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:
GET http://localhost:8080/rest
/services
/sample_OrderService
/calculateTotalAmount?orderId=123
Authorization: Bearer {{access_token}}
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:
@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:
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:
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:
{
"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 byDateTimeDatatype
. This datatype implementation uses the ISO_DATE_TIME format where the date and time parts are separated withT
, for example2011-12-03T10:15:30
. -
Parameters of
java.sql.Date
type are handled byDateDatatype
which uses ISO_DATE format, for example2011-12-03
. -
Parameters of
java.sql.Time
type are handled byTimeDatatype
which uses ISO_TIME format, for example10: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:
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:
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:
GET http://localhost:8080/orders/calculateTotalAmount?orderId=123
The result contains the calculation result exposed as JSON as well as the defined HTTP headers:
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:
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
:
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
);
}