. . .

Logic flow and architecture

This page is an in-depth guide about the logic flow and architecture of ApiOmat services. It contains the following sections:


Spring and Spring Boot basic concepts

Each service is a Spring Boot project, the main entry point is the [ServiceName]Application.java class. This class is annotated with @SpringBootApplication, which provides multiple Spring annotations. We will focus on one in particular: @ComponentScan.

The job of @ComponentScan is to search for classes that are annotated with @Component (or annotations that extend/are annotated with it, such as @Controller or @Service). By default, @ComponentScan searches the base package of the class in which it is defined and all sub packages. For each class discovered in this way, a Spring bean will be added to the service's Spring application context. See this Baeldung article for further information on @ComponentScan.
To summarize it in a simple way: Beans are singleton instances of a class, and the Spring application context is like a cache which contains all these singleton instances so they can be accessed by any of the service's components.
If you are not familiar with Spring, please refer to the Spring Framework documentation.

Your service is a web application that can be started within an embedded server, it has the spring-boot-starter-tomcat dependency which provides an embedded Tomcat server. The API definition is made easy thanks to Spring Web annotations provided by the spring-boot-starter-web dependency. The endpoints of your web application are defined using the @RequestMapping annotation, which maps your controller methods to a path, an HTTP method type (GET, POST, etc.) and sometimes the produced and consumed content type(s).

Service logic flow

As described before, the endpoints of your service are defined via Spring Web annotations on your controllers. For a good introduction to Spring Boot starters and especially the Web starter, see this Baeldung article. Each of your data models will have a controller interface and its implementation, the methods generated there depend on the types of your data model's attributes. More information on generated content based on data can be found in the Data oriented content documentation.
We will use the TutorialService from the introduction tutorial as an example. The controller package of that service contains the IGroceryApi.java interface and its GroceryApiController.java implementation:

images/download/attachments/71320458/Screen-Shot-2020-02-25-at-09.43.21.png

The IGroceryApi.java interface defines the API for the Grocery data model. The logic is first applied within GroceryApiController.java which passes the call on to the service layer. The service instance is autowired within the controller implementation, which means that an attribute of the service type is present in the controller class and annotated with Spring's @Autowire annotation, used to retrieve components from the application context: When the controller component is instantiated, the instance of the service attribute's class will be retrieved from the application context and the component's attribute will be set to that instance.

GroceryApiController.java
@Controller
public class GroceryApiController implements IGroceryApi 
{
@Autowired
@Qualifier("groceryService")
private IBaseDataModelService<Grocery> service;
 
...
}

Now let's take a closer look at this service attribute. The type is IBaseDataModelService.java with type parameter Grocery, which is the class for which this controller is providing an API. Also, note the @Qualifier annotation with value "groceryService". We explained earlier how the @Autowired annotation is used to retrieve a specific component type from the application context - the @Qualifier annotation allows you to define multiple components of the same type in the application context by giving each of them an identifier and retrieving them via said identifier, as we are doing here to retrieve the service for handling Grocery logic.

The IBaseDataModelService is an interface provided by the brewer-base library. It has the implementation BaseDataModelServiceImpl which contains the global logic applied before and after any service-specific logic (e.g. security checks). This implementation has a constructor that takes an IDataModelService parameter. An implementation of the IDataModelService is generated for each of your service's data models, extending an abstract implementation that contains default logic (non-transient, which means that the data is saved in ApiOmat's database).
In the case of TutorialService, the service package contains GroceryApiServiceImpl.java which implements IDataModelService and extends the default implementation AbstractGroceryApiServiceImpl.java:

images/download/attachments/71320458/Screen-Shot-2020-02-25-at-09.43.59.png

For each of your IDataModelService implementations, a bean of IBaseDataModelService is created within your service configuration, in the case of TutorialService this is the class TutorialServiceConfiguration.java:

TutorialServiceConfiguration.java
@Configuration
public class TutorialServiceConfiguration
{
@Autowired
IDataModelService<Grocery> groceryService;
 
/**
* @return bean of service implementation of Grocery
*/
@Bean( name = "groceryService" )
public IBaseDataModelService<Grocery> groceryImplService( )
{
return new BaseDataModelServiceImpl<Grocery>( this.groceryService );
}
 
...
}

Here's a quick summary of the logic flow:

  1. Controllers are calling methods of the qualified autowired instance of IBaseDataModelService.

  2. IBaseDataModelService has an implementation of type BaseDataModelServiceImpl.

  3. The implementation instance of IDataModelService is provided to BaseDataModelServiceImpl in the bean creation method of the service configuration class.

  4. BaseDataModelServiceImpl is calling methods of IDataModelService.

  5. IDataModelService logic is either within your specific service implementation (e.g. GroceryApiServiceImpl) or the default abstract implementation (e.g. AbstractGroceryApiServiceImpl).

images/download/attachments/71320458/GeneratedServiceArch.png

Your logic has to be implemented in the <Class>ApiServiceImpl class by overriding the logic from Abstract<Class>ApiServiceImpl.
So in our example, you would implement your logic in GroceryApiServiceImpl, which inherits from AbstractGroceryApiServiceImpl.

Within the java package, everything except the service implementation class and IDataModelService objects will be overwritten by service generation when merging with existing sources from Innkeeper!

Service hooks

The IDataModelService provides multiple hooks to be used by the API controllers, and for which specific logic can be implemented in the ApiServiceImpl classes for each of your data models.
Here is the exhaustive list with some explanation:

// Count the number of data model instances stored in the database.
// The result can be filtered using a query "q"
// If withClassnameFilter is true (default), only instances with the exact class name will be counted, which filters out child classes.
public int count( String appName, String q, boolean withClassnameFilter );
 
// true if module implementation is transient, else false. Default abstract implementation always returns the class model value.
public boolean isTransient( );
 
// Save the given data model object, model ID should always be null in this case
public String save( String appName, T model );
 
// Post an updated data model
// Param "id" should be ID in case of persistent (nontransient) behavior, foreign ID in case of transient behavior. It is used here and in other methods to identify the data model instance.
public void update( String appName, String id, T model );
 
// Load a single data model instance.
public T load( String appName, String id );
 
// Load all data model instances.
// If withClassnameFilter is true (default), only instances with the exact class name will be counted, which filters out child classes.
// If withReferencedHrefs is true (default is false), the hrefs of referenced classes will be returned too.
public List<T> loadAll( String appName, String q, boolean withClassnameFilter, boolean withReferencedHrefs );
 
// Delete a single data model instance.
public void delete( String appName, String id );
 
// Delete all data model instances. Can be filtered via query "q".
public void deleteAll( String appName, String q );
 
// Add a reference to a data model instance.
// Z corresponds to the reference type. 
// IRefAttributeDefinition is an interface from the "brewer-base" library that is used to define a reference attribute of your data model. Each of your data models has an autogenerated enum of its reference definitions. The param "refDefinition" is used here and in the following methods to identify where the reference is stored in the data model (which attribute).
public <Z extends AbstractDataModel> String addReference( String appName, String dataModelId, IRefAttributeDefinition<Z> refDefinition, Z refObj );
 
// Get a list of references stored for the given attribute of a data model instance. 
// The list will contain one element for a single reference attribute, several for a reference collection attribute.
public <Z extends AbstractDataModel> List<Z> loadReference( String appName, String dataModelId, IRefAttributeDefinition<Z> refDefinition, String q, boolean withReferencedHrefs );
 
// Remove a reference from a data model instance. This will not delete the referenced object - only remove its value from the data model instance's attribute.
// refId is the ID of the referenced object to be removed
public <Z extends AbstractDataModel> void removeReference( String appName, String dataModelId, IRefAttributeDefinition<Z> refDefinition, String refId );
 
// Count the references stored in a data model instance's given attribute. Can be filtered via query "q".
public <Z extends AbstractDataModel> int countReference( String appName, final String dataModelId, IRefAttributeDefinition<Z> refDefinition, String q );
 
// Upload a file as a multipart and save it to the given attribute of a data model instance.
// IStaticDataAttributeDefinition is an interface from the "brewer-base" library that is used to define a static data attribute of your data model. Each of your data models has an autogenerated enum of its static data attributes. The param "fileDefinition" is used here and in the following methods to identify where the file is stored in the data model (which attribute).
// apiKey is the ApiOmat application key.
// system is the currently used system (LIVE, STAGING or TEST).
// format is the file type (e.g. "txt") without a preceding dot.
public String postFile( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, MultipartFile file, String apiKey, String system, String format, String name );
 
// Upload a file as an octet stream and save it to the given attribute of a data model instance.
public String postFileAsOctet( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, Resource file, String format, String name );
 
// Load a file resource saved in the given attribute of a data model instance.
// fileId is the file's ID
// token is the authentication token to access the resource. This is not mandatory (only if the attribute's resource access is restricted, can be set via Dashboard).
// If asstream is true (default false), the file will be returned as a stream.
public StaticDataResource loadFile( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, String fileId, String token, String apiKey, String system, boolean asstream );
 
// Delete a file resource saved in the given attribute of a data model instance.
public void deleteFile( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, String fileId );
 
// Create a transient access key to the given file resource saved in the given attribute of a data model instance.
// validity is the lifetime of the key in seconds.
// If oneTime is true, the key can only be used once.
public String createFileKey( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, Long validity, boolean oneTime, String fileId, String apiKey, String system, boolean asstream );
 
// Returns metadata of a file saved in the given attribute of a data model instance; the response headers will contain the following fields:
// name - Name of the file
// size - Size of the file in bytes
// extension - Extension of the file
public MultiValueMap<String, String> getFileHead( String appName, String dataModelId, IStaticDataAttributeDefinition fileDefinition, String fileId, String apiKey, String system );
 
// Upload an image as a multipart and save it to the given attribute of a data model instance.
// format is the image format (e.g. "png") without a preceding dot.
public String postImage( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, MultipartFile image, String apiKey, String system, String format, String name );
 
// Upload an image as an octet stream and save it to the given attribute of a data model instance.
public String postImageAsOctet( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, Resource image, String format, String name );
 
// Load an image resource saved in the given attribute of a data model instance.
// imageId is the ID of the image resource
// token is the authentication token to access the resource. This is not mandatory (only if the attribute's resource access is restricted, can be set via Dashboard).
// show affects the response headers: When set to false, the image is returned as an attachment, else as content with its resource MIME type as the content type header.
// The transcoding configuration can be used to resize the image or change the format or color.
public StaticDataResource loadImage( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, String imageId, String token, String apiKey, String system, boolean show, TranscodingConfiguration conf );
 
// Delete an image resource saved in the given attribute of a data model instance.
public void deleteImage( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, String imageId );
 
// Create a transient access key to the given image resource saved in the given attribute of a data model instance.
// validity is the lifetime of the key in seconds.
// If oneTime is true, the key can only be used once.
public String createImageKey( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, Long validity, boolean oneTime, String imageId, String apiKey, String system, boolean show, TranscodingConfiguration conf );
 
// Returns metadata of an image saved in the given attribute of a data model instance; the response headers will contain the following fields:
// name - Name of the image
// size - Size of the image in bytes
// extension - Extension of the image
public MultiValueMap<String, String> getImageHead( String appName, String dataModelId, IStaticDataAttributeDefinition imageDefinition, String imageId, String apiKey, String system, TranscodingConfiguration conf );

The default behavior of these hooks is persistent; Abstract<Class>ServiceImpl classes are always using Feign clients to call YAMBAS which then does database calls.

The next section explains how the Feign framework is used and how to change this persistent behavior to transient.

Transient and persistent logic

Feign clients

You may already have noticed that within the service package, an ApiClient class is generated for each data model of your service. This class uses the Feign framework to call YAMBAS. Feign makes it easy to create web clients, in our case we are using it to proxy requests to YAMBAS. For a more in-depth guide about Feign, please refer to the Spring Cloud Feign documentation.

In practice, it means that this code from the default abstract implementation of the Grocery service:

AbstractGroceryApiServiceImpl.java
this.client.createNewGrocery( appName, model );

Is actually calling YAMBAS to save a Grocery object. Let's have a look at how Feign is handling the call to YAMBAS logic:

  1. Feign logic is imported via the spring-cloud-starter-openfeign dependency.

  2. Feign logic is applied by annotating an interface class with @FeignClient, e.g. the class GroceryApiClient in the case of our TutorialService:

    GroceryApiClient.java
    @FeignClient( name="${tutorialservice.coreservice.name}", path = "${tutorialservice.coreservice.pathprefix}", configuration = AOMFeignClientConfig.class )
    public interface GroceryApiClient
    1. name is the Consul ID of the service we want to communicate with.

    2. path is the global path that should be appended to every request mapping.

    3. configuration is the class providing specific Feign components configured for this client.

  3. The class AOMFeignClientConfig contains specific beans defined for all Feign clients communicating with YAMBAS, which override some of the default implementations from the FeignClientsConfiguration class of Feign (so you can, but don't have to, provide your custom implementations of beans). This default configuration is imported in the ApiServiceImpl classes:

    GroceryApiServiceImpl.java
    @Service
    @Import( FeignClientsConfiguration.class )
    public class GroceryApiServiceImpl extends AbstractGroceryApiServiceImpl

    It contains some beans which are only added to the application context if they are missing (i.e. not provided by AOMFeignClientConfig), e.g.:

    FeignClientsConfiguration.java
    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() { ... }

  4. The main application class of the generated service is annotated with @EnableFeignClients, which enables discovery of Feign clients. In the case of TutorialService, the main class is TutorialServiceApplication.java:

    TutorialServiceApplication.java
    @SpringBootApplication
    @EnableFeignClients
    public class TutorialServiceApplication extends SpringBootServletInitializer { ... }

  5. The service name and path prefix of YAMBAS are set in the service's application.yml file, e.g. for TutorialService:

    application.yml
    tutorialservice:
    coreservice:
    name: YAMBAS
    pathprefix: /yambas/rest

  6. The Feign client interface defines methods and maps them to concrete endpoints of YAMBAS so that calls to these methods will be redirected to YAMBAS, e.g. in TutorialService with the deleteGrocery method of GroceryApiClient.java:

    GroceryApiClient.java
    @RequestMapping(method = RequestMethod.DELETE, value ="/apps/{appName}/models/TutorialService/v/${spring.application.version}/Grocery/{dataModelId}")
    void deleteGrocery(@PathVariable("appName") final String appName, @PathVariable("dataModelId") final String dataModelId);

    Internally, Feign uses a discovery client and load balancing mechanism to make HTTP calls without having to define a whole web client.

  7. The following additional configuration is set in application.yml for all YAMBAS Feign clients:

    application.yml
    feign:
    client:
    config:
    YAMBAS:
    connectTimeout: 5000
    readTimeout: 5000
    requestInterceptors:
    - com.apiomat.service.base.feign.RequestHeaderRequestInterceptor
    1. connectTimeout and readTimeout are in milliseconds.

    2. RequestHeaderRequestInterceptor is a custom implementation provided by the brewer-base library which copies all HTTP headers from the incoming request to your service and adds them to the proxied request to YAMBAS (so that the proxied request will have any headers required by YAMBAS, e.g. the API key).

Switching from persistent to transient behavior

To change your implementation to transient, override the abstract implementation to avoid using Feign clients (because they call YAMBAS). For e.g. a transient Grocery in the TutorialService, the save method could be overridden in GroceryApiServiceImpl.java:

GroceryApiServiceImpl.java
@Override
public String save( final String appName, final Grocery model )
{
// do nothing
return "";
}

Additionally, you may want to provide two services, one with transient behavior and one with persistent behavior. We provide a commented example of how you may do it:

GroceryApiServiceImpl.java
@Service
@Import( FeignClientsConfiguration.class )
/* Example on how to handle different implementations for this service, by using a conditional annotation to load only one
* of the services in the context. Make sure that the condition is exclusive so Spring will only create a bean of one of your
* implementations. */
// @ConditionalOnProperty( prefix = "tutorialservice", name = "state", havingValue = "nontransient" )
public class GroceryApiServiceImpl extends AbstractGroceryApiServiceImpl

In practice, you may create two implementations - GroceryApiTransientServiceImpl.java:

GroceryApiServiceImpl.java
@Service
@Import( FeignClientsConfiguration.class )
// Added to the application context if property "tutorialservice.state" has value "transient"
@ConditionalOnProperty( prefix = "tutorialservice", name = "state", havingValue = "transient" )
public class GroceryApiTransientServiceImpl extends AbstractGroceryApiServiceImpl
{
// Override parent methods to avoid database call
 
@Override
public String save( final String appName, final Grocery model )
{
// Do nothing
return "";
}
...
}

And GroceryApiNonTransientServiceImpl.java:

GroceryApiServiceImpl.java
@Service
@Import( FeignClientsConfiguration.class )
// Added to the application context if property "tutorialservice.state" has value "nontransient"
@ConditionalOnProperty( prefix = "tutorialservice", name = "state", havingValue = "nontransient" )
public class GroceryApiNonTransientServiceImpl extends AbstractGroceryApiServiceImpl
{
// Keep default abstract parent behavior
}

With the following configuration in your application.yml:

application.yml
tutorialservice:
# to have a bean of GroceryApiNonTransientServiceImpl in your application context
state: nontransient
# to have a bean of GroceryApiTransientServiceImpl in your application context
# state: transient

API request flow

In a nutshell, when someone makes a request to your service API:

  1. Controller API layer handles the request and calls the global service implementation provided by the brewer-base library.

  2. Global service implementation applies its "before" logic and calls your class-specific logic.

  3. The class-specific logic will be the abstract logic, by default, or your specific implementation.

  4. If your service is non-transient, YAMBAS logic will be applied by using the class-specific Feign client.

  5. Global service implementation applies its "after" logic and returns to the controller layer.

  6. Controller API layer receives the result and returns a correctly formatted HTTP response.

images/download/attachments/71320458/SequenceArch.png

Exception handling

Spring Boot provides a smooth way to handle exceptions of a specific type that may be thrown while executing REST logic. Here is how exceptions are handled in your generated service's API:

  1. The brewer-base library provides the autoconfigured exception handling class AOMErrorExceptionHandler. This class is annotated with @ControllerAdvice, notifying Spring logic that it provides a method annotated with @ExceptionHandler to be shared by all controller classes within the application context.

  2. AOMErrorExceptionHandler extends the default ResponseEntityExceptionHandler of Spring that handles multiple types of exceptions that could be thrown by any REST API. It also contains a method that specifically handles AOMException:

    AOMErrorExceptionHandler.java
    @ExceptionHandler( { AOMException.class } )
    public ResponseEntity<String> handleFeignException( AOMException fe, WebRequest request )
  3. AOMException is a simple runtime exception POJO that extends FeignException and provides a message and status code. When an AOMException is thrown by the logic called within your REST API, it's handled by the AOMErrorExceptionHandler#handleFeignException method mentioned above:

    1. Message and status are extracted from the AOMException.

    2. A JSONObject is created with fields "message", "status" and "timestamp".

    3. "message" and "status" correspond to the extracted values and "timestamp" is the value of Instant.now() (Java 8 class) at the time of the exception getting handled.

    4. The ResponseEntity contains the previously defined JSONObject value as a String body, the returned HTTP code depends on the status and is compliant to YAMBAS error codes:

      AOMErrorExceptionHandler.java
      if ( statusCode >= 700 && statusCode < 800 )
      {
      httpStatus = HttpStatus.BAD_REQUEST;
      }
      else if ( statusCode >= 800 && statusCode < 820 || statusCode >= 1800 && statusCode < 1820 )
      {
      httpStatus = HttpStatus.NOT_FOUND;
      }
      else if ( statusCode >= 820 && statusCode < 829 || statusCode >= 1820 && statusCode < 1829 )
      {
      httpStatus = HttpStatus.FORBIDDEN;
      }
      else if ( statusCode >= 830 && statusCode < 840 || statusCode >= 1830 && statusCode < 1840 )
      {
      httpStatus = HttpStatus.CONFLICT;
      }
      else if ( statusCode >= 840 && statusCode < 850 || statusCode >= 1840 && statusCode < 1850 )
      {
      httpStatus = HttpStatus.UNAUTHORIZED;
      }
      else if ( statusCode >= 900 )
      {
      httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
      }

      If the status code is under 700 and above/equal to 599, an "internal server error" code will be returned, otherwise the status's HTTP code value will be directly evaluated:

      HttpStatus.valueOf( statusCode );
  4. Additionally, your generated Feign clients are configured so that any thrown FeignExceptions will be wrapped to an AOMException in the AOMErrorDecoder class and handled by the AOMErrorExceptionHandler mechanism:

    AOMFeignClientConfig.java
    @Bean
    public ErrorDecoder feignErrorDecoder( )
    {
    return new AOMErrorDecoder( );
    }

If you want a detailed explanation of the different ways to achieve REST exception handling in Spring, see this Baeldung article.