Search

Providing Services

The Service Provisioning APIs can be used to register event handlers on services. In CAP everything that happens at runtime is an event that is sent to a service. With event handlers the processing of these events can be extended or overridden. Event handlers can be used to handle CRUD events, implement actions and functions and to handle asynchronous events from a messaging service.

Content

Introduction to Event Handlers

CAP allows you to register event handlers for events on the services defined in your CDS model. Event handlers enable you to add custom business logic to your application by either extending the processing of an event, or by completely overriding its default implementation. Typical events that happen at runtime are CRUD events (CREATE, READ, UPDATE, UPSERT, and DELETE). These events are sent by HTTP-based protocol adapters (for example an OData V4 protocol adapter) to the respective CDS service. The CAP Java SDK provides default event handlers (also known as Generic Providers) that handle CRUD operations out-of-the-box and implement the handling of many CDS annotations. Therefore, it’s most common that event handlers for CRUD events extend the event processing (Before and After phase).

Actions and functions can be used to add custom synchronous events to your services. For such custom events CAP doesn’t provide default event handlers. For these events, it’s therefore common to register event handlers that define the default implementation (On phase).

Events usually have parameters and a return value. The CRUD events in CAP are mapped to CQN statements. If you have a READ event, a CQN Select statement is provided as a parameter to the event handlers. The return value is a list of entity objects, that were selected by the query. Similarly, a CREATE event gets a CQN Insert statement as a parameter. This CQN Insert statement also contains the entity data to be inserted. The list of entity objects that were inserted is provided as a return value. The remaining CRUD events UPDATE, UPSERT, and DELETE also have a respective CQN statement type.

For actions and functions, the parameter types and return type are directly modeled in the CDS definition. The CAP Java SDK provides a way to access parameters and return values of CRUD events and custom events in a type-safe way.

Event Phases

Events are processed in three phases that are executed consecutively: Before, On, and After. When registering an event handler the phase in which the event handler should be called needs to be specified. In Java, an annotation exists for each event phase (@Before, @On, and @After) to let the framework know which phase of the event processing is to be handled.

It’s possible to register multiple event handlers for each event phase. Handlers within the same event phase are executed one at a time and therefore never concurrently. In case concurrency is desired, it needs to be explicitly implemented within an event handler. There’s also no guaranteed order in which the handlers of the same phase are called. It’s therefore recommended to put code that requires ordering of certain steps into a single event handler.

The following subsections describe the semantics of the three phases in more detail:

Before

The Before phase is the first phase of the event processing. This phase is useful for filtering, validation, and other types of preprocessing of the incoming parameters of an event. There can be an arbitrary number of Before handlers per event. The processing of the Before phase is completed when one of the following conditions applies:

  • All registered Before handlers were successfully called. Execution continues with the On phase.
  • A handler completes the event processing by setting a result value or setting the state of an event to completed. In this case, any remaining registered Before and On handlers are skipped and execution continues with the After phase.
  • A handler throws an exception. In this case, event processing is terminated immediately.

On

The On phase is started after the Before phase, as long as no return value is yet provided and no exception occurred. It’s meant to implement the core processing of the event. There can be an arbitrary number of On handlers per event, although as soon as the first On handler successfully completes the event processing, all remaining On handlers are skipped.

The On phase is completed when one of the following conditions applies:

  • A handler completes the event processing by setting a result value or setting the state of an event to completed. In this case, any remaining registered On handlers are skipped and execution continues with the After phase.
  • A handler throws an exception. In this case, event processing is terminated immediately.

If after the On phase, no handler completed the event processing, it’s considered an error and the event processing is aborted with an exception.

After

The After phase is only started after the On phase is completed successfully. Handlers are therefore guaranteed to have access to the result of the event processing. This phase is useful for post-processing of the return value of the event or triggering side-effects. A handler in this phase can also still abort the event processing by throwing an exception. No further handlers of the After phase are called in this case.

Event Contexts

In CAP Java, the EventContext is the central interface, that provides information about the current event to the event handler. It provides general information, like the currently processed event, the target entity of the event and the service that the event was sent to. Additional information, the EventContext provides access to:

  • User information
  • Headers and query parameters
  • CDS model
  • Catalog of all available services
  • Changeset context

The event context also stores all parameters of the event and the return value. From the EventContext interface the parameters and return value can be accessed and modified:

EventContext context = EventContext.create("myEvent", null);

// set parameters
context.put("parameter1", "MyParameter1");
context.put("parameter2", 2);

srv.emit(context); // process event

// access return value
Object result = context.get("result");

Both of these methods aren’t aware of the type of the parameter or return value, in context of the specific event. They also require a key, which is used to store and retrieve the parameter or return value from a map. As this is cumbersome and error-prone, the CAP Java SDK provides the ability to overlay an event context with a specialized event context interface. This interface is aware of the parameter types and the return type of a certain event.

For each event that the CAP Java SDK provides out-of-the-box (for example the CRUD events) a specialized event context is provided. This specialized event context interface can be used to access the parameters and the return value of a certain event through normal getter and setter methods. These methods are also aware of the concrete type of the parameter or return value.

An example of such an event context is the CdsReadEventContext interface. The READ event has a CQN Select as parameter and returns a list of entity data, represented as a Result object. The following example shows how such an interface can be used with an existing event context:

CdsReadEventContext context = genericContext.as(CdsReadEventContext.class);
CqnSelect select = context.getCqn();
context.setResult(Collections.emptyList());
Result result = context.getResult();

It’s important to note, that these getter and setter methods, still operate on the simple get/put API shown in the previous example. They just provide a type-safe layer on top of it. The interfaces also don’t need to be implemented. Within the as method, Java proxies are used to enable this. This flexibility enables the ability to create such interfaces also for custom actions and functions, defined in the CDS model.

Recommendation: Use these specialized type-safe EventContext interfaces whenever possible.

Completing the Event Processing

The EventContext interface also provides the API to complete the event processing. Indicating the completion of the event, is important to successfully finish the On phase of an event. If the event doesn’t have any return value, the completion can be indicated by triggering the context.setCompleted() method.

For events that specify a return value, the context.setResult(...) method of the specialized event context interface can be used to set the return value. Calling this method automatically sets the event to completed.

If the generic event context API is used, setting the result and setting the event to completed are two dedicated steps. Please note, that by convention the return value is expected under the key result in the generic event context:

Object myResult = ...
context.put("result", myResult);
context.setCompleted();

Defining Custom EventContext Interfaces

When implementing actions or functions, it can be useful to define custom EventContext interfaces to allow type-safe access to the parameters and the returned value of the action or function. CAP provides a configuration option in the CAP Java SDK Maven Plugin to generate these interfaces for bound/unbound actions and functions based on the CDS model.

The following example shows a generated EventContext interface.

@EventName("myAction")
public interface MyActionContext extends EventContext {

        String getParam1();

        void setParam1(String param1);

        CqnSelect getCqn();

        void setResult(Integer result);

        Integer getResult();

        static MyActionContext create(String entityName) {
    		return EventContext.create(MyActionContext.class, entityName);
  	}
}

The @EventName annotation ensures that the custom EventContext interface (here MyActionContext) is only used on EventContext instances, that correspond to the specified event. The getter and setter methods are named after the name of the parameter in the CDS model. Any difference in the naming of the parameter is specified using the @CdsName annotation. The create method invokes the EventContextFactory to create a new proxy object of type MyActionContext.

Registering Event Handlers

In Java event handlers are implemented by methods in event handler classes. Event handler methods need to be annotated with a @Before, @On, or @After annotation and can have a flexible method signature.

Event Handler Classes

Event handler classes can contain event handler methods. You can use them to group event handlers for a specific service or even a specific entity. The class can also define arbitrary methods, which aren’t event handler methods, to provide functionality reused by multiple handler methods.

When running in Spring Boot, event handler classes are defined as a Spring bean. Therefore, you can use the full flexibility of Spring beans, such as Dependency Injection or Scopes within these classes.

The following example defines an event handler class in a Spring context:

import org.springframework.stereotype.Component;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.ServiceName;

@Component
@ServiceName("AdminService")
public class AdminServiceHandler implements EventHandler {

}

Every event handler class needs to implement the EventHandler marker interface. Implementing this interface is required, to identify the class as an event handler class.

The @ServiceName annotation can be used to specify the default service, which the event handlers are registered on. In a CDS service, the fully qualified name, including the namespace, has to be specified. When using the static model, generated by the CAP Java SDK Maven Plugin, String constants for these names are available. The service defined by this annotation can be overridden through the annotations on the event handler methods.

Event Handler Methods

Event handler methods are methods within an event handler class, that are annotated with one of the following annotations: @Before, @On, or @After. The annotation defines, during which phase of the event processing the event handler is called.

Each of these annotations can define the following three attributes:

  • service: The service attribute can be used to specify the service the event handler is registered on. It’s optional, if a @ServiceName annotation is specified on class-level.

  • event: The event attribute specifies the event the handler wants to extend or override. It’s optional, if the event can be inferred through a specific EventContext argument in the handler signature. If no event can be inferred or is specified in the attribute, the attribute defaults to "*", which matches any event. The attribute can be used to specify multiple events by specifying them in an array. The event handler is invoked in case one of the events matches.

  • entity: The entity attribute specifies the target entity that needs to be set, in order for the handler to be invoked. It’s optional, if the entity can be inferred through a POJO-based argument in the handler signature. If no entity can be inferred or is specified in the attribute, the attribute defaults to "*", which matches any entity. The attribute can be used to specify multiple entities by specifying them in an array. The event handler is invoked in case one of the entities matches.

The Java Service interfaces (for example the CdsService interface) define String constants for the built-in events they can process. For the service and entity attribute the fully qualified names, including the namespace, need to be used. When using the static model, generated by the CAP Java SDK Maven Plugin, String constants for these names are available. It’s recommended to use these constants in annotation attributes, to prevent unexpected behavior caused by typos.

Here are some examples of different event handler methods:

@Before(event = { "CREATE", "UPSERT" }, entity = "AdminService.Books")
public void beforeCreateBooks(EventContext context) {
    // shows how an event handler can be registered on multiple events
}

@On(service = "CatalogService", event = "READ")
public void readAnyEntityInCatalogService(EventContext context) {
    // shows how the default service on class-level can be overriden
    // shows how to register on any entity
}

@After(event = CdsService.EVENT_READ, entity = Books_.CDS_NAME)
public void afterReadBooks(EventContext context) {
    // use the available string constants for events and entities!
}

The basic signature of an event handler method is void process(EventContext context). However, handler methods registered via annotations can have a more flexible handler signature. Handler methods don’t necessarily have to be public methods. They can also be methods with protected, private, or package visibility.

Parameters and Return Types

Registering event handler methods via Java annotations provides the ability to have flexible handler signatures. The basic signature of an event handler method is void process(EventContext context). Everything can be achieved using this signature. However, it doesn’t provide the highest level of comfort. Event handler signatures can vary on three levels:

  • EventContext arguments
  • POJO-based arguments
  • Return type

All types of arguments can be combined as desired. It’s also valid for event handler methods to have no arguments at all.

Event Context Arguments

Every handler method can get access to the EventContext. The EventContext interface is a general interface, but CAP also provides the ability to use (custom) event-specific interfaces. You can directly refer to these event-specific interfaces in your method arguments. In that case, the general interface is overlaid with the event-specific one. This behavior works for events defined by CAP (for example, CRUD events), as well as for custom events, for example defined through actions or functions.

During startup of the application, the event context arguments are automatically validated against the events the handlers are registered on. If an event doesn’t match the requested event context interface, the startup of the application fails. This failure handling ensures, that such errors are found early in the development process.

If an event-specific event context argument is used, the event definition in the annotation can be omitted. The event handler is automatically registered on the event, that corresponds to the event context interface. The mapping between an event context interface and its event, is done through the @EventName annotation on the event context interface.

The following examples showcase the usage of different types of event context arguments:

@After(event = CdsService.EVENT_READ, entity = Books_.CDS_NAME)
public void afterReadBooks(CdsReadEventContext context) {
    // use the event context specific to READ events
}

@Before(entity = Books_.CDS_NAME)
public void beforeCreateBooks(CdsCreateEventContext context) {
    // CREATE event can be omitted from the annotation
    // it is inferred from the CdsCreateEventContext argument
}

@Before(event = { CdsService.EVENT_UPDATE, CdsService.EVENT_UPSERT }, entity = Books_.CDS_NAME)
public void wrongBeforeUpdateBooks(CdsUpdateEventContext context) {
    // WRONG! This will throw an error during startup
    // the CdsUpdateEventContext does not match to the UPSERT event
    // in this case only the generic EventContext argument can be used
    // you can use the EventContext.as(Class) method as alternative
}

POJO-Based Arguments

Most of the time, event handler methods are interested in the entity data, that was provided by the client or that is provided to the client as a result of the event. The CAP Java SDK code generator generates a POJO interface for every entity in the CDS model to enable typed access to entity data. These POJO interfaces can be directly used in the event handler method signature.

Getting access to the entity data directly from the method arguments is only supported for the basic CRUD events and their draft-specific counterparts. Depending on the phase of the event processing, either the entity data provided by the client (Before and On), or the entity provided to the client (After) is provided as argument. For events that don’t have entity data in their incoming parameters (for example READ and DELETE) null is provided for POJO-based arguments on handlers of the Before or On phase. By default a DELETE event doesn’t provide the deleted entity data in its return value. Therefore, POJO-based arguments are also set to null on handlers of the After phase for this event.

The entity data for POJO-based arguments during Before and On phases is obtained from what is available in the CQN statement. During the After phase, the entity data is obtained from the Result object.

As usually a collection of entities can be present in CQN statements or in Result objects, the POJO-based arguments can be either List<Entity>, Stream<Entity> or just Entity (Entity being the POJO interface). If no collection type is used an error occurs at runtime, if multiple entities are provided in the CQN or Result. It’s therefore preferred to use the collection-based arguments.

Similarly, as for event context arguments, a validation of the POJO-based arguments is performed during startup of the application. If an entity doesn’t match the entity type of the POJO-based argument, the startup of the application fails.

If a POJO-based argument is used, the entity definition in the annotation can be omitted. The event handler is automatically registered on the entity, that corresponds to the POJO interface. The mapping between a POJO interface and its entity, is done through the @CdsName annotation on the POJO interface.

The following examples showcase the usage of different POJO-based arguments:

@After(event = CdsService.EVENT_READ, entity = Books_.CDS_NAME)
public void afterReadBooks(List<Books> books) {
    // directly get access to read books
}

@Before(event = CdsService.EVENT_CREATE)
public void beforeCreateBooks(Stream<Books> books) {
    // get access to client data to be inserted
    // entity can be omitted from the annotation
    // it is inferred from the Books POJO interface
}

Flexible Return Types

Event handlers that are registered on the Before or On phase might want to set the return value of the event to complete the event processing. This is possible programmatically on the EventContext interface. For the basic CRUD events and their draft-specific counterparts, the event handler method can directly return any object that implements Iterable<? extends Map<String, Object>>. The Result object type used by CAP Java SDK meets these requirements.

When returning such an object, it’s automatically set as result on the processed event context and the event context is set to completed.

The following examples showcase this:

@On(event = CdsService.EVENT_READ, entity = Books_.CDS_NAME)
public Result readBooks(CdsReadEventContext context) {
    // return Result objects
    Result result = db.run(context.getCqn());
    return result;
}

@Before(event = CdsService.EVENT_READ, entity = Authors_.CDS_NAME)
public List<Authors> readAuthors(EventContext context) {
    // return lists of POJO objects
    Authors author = Struct.create(Authors.class);
    return Arrays.asList(author);
}

@On(event = CdsService.EVENT_READ, entity = Orders_.CDS_NAME)
public List<Map<String, Object>> readOrders(EventContext context) {
    // return any Iterable<Map<String, Object>> type
    Map<String, Object> ordersMap = new HashMap<>();
    return Arrays.asList(ordersMap);
}

Implementing Event Handlers

When implementing event handlers, it’s important to understand certain concepts of CAP applications. In the following section, some of these concepts are explained. It’s important to keep in mind, that when running on Spring Boot, event handlers have the full flexibility of Spring beans. If the CAP Java SDK doesn’t provide the required functionality or abstraction, it’s often already available within Spring Boot.

Event Flow

It’s important to understand how CAP triggers events on services at runtime, to register the correct event handlers when extending or overriding the processing of an event. The CAP Java SDK defines events for the standard CRUD operations:

  • CREATE
  • READ
  • UPSERT
  • UPDATE
  • DELETE

Static constants exist for these event names on the CdsService interface, which also defines the Consumption API of these events.

These events are used to implement HTTP-based protocol adapters, like the one for OData V4. OData is built upon the HTTP verbs. It’s therefore interesting to know, how these HTTP verbs are mapped to CAP’s CRUD events:

  • POST –› CREATE
  • GET –› READ
  • PUT –› UPDATE (or CREATE, if the entity didn’t yet exist)
  • PATCH –› UPDATE (or CREATE, if the entity didn’t yet exist)
  • DELETE –› DELETE

In CAP Java versions < 1.9.0, the UPSERT event was used to implement OData V4 PUT requests. This has been changed, as the semantics of UPSERT didn’t really match the semantics of the OData V4 PUT.

Events on deeply structured documents, are only triggered on the target entity of the CRUD CQN statement. This means, that if a document is created or updated, events aren’t automatically triggered on composition entities. Also when reading a deep document, leveraging expand capabilities, READ events aren’t triggered on the expanded entities. The same applies to a deletion of a document, which doesn’t automatically trigger DELETE events on composition entities to which the delete is cascaded.

When implementing validation logic, this can be handled like shown in the following example:

@Before(event = CdsService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void validateOrders(List<Orders> orders) {
    for(Orders order : orders) {
        if (order.getItems() != null) {
            validateItems(order.getItems());
        }
    }
}

@Before(event = CdsService.EVENT_CREATE, entity = OrderItems_.CDS_NAME)
public void validateItems(List<OrderItems> items) {
    for(OrderItems item : items) {
        if (item.getAmount() <= 0) {
            throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid amount");
        }
    }
}

In the example, the OrderItems entity exists as a composition within the Items element of the Orders entity. When creating an order a deeply structured document can be passed, which contains order items. For this reason, the event handler method to validate order items (validateItems) is called as part of the order validation (validateOrders). In case an order item is directly created (for example through a containment navigation in OData V4) only the event handler for validation of the order items is triggered.

Actions and Functions

Actions and functions define custom events. They can therefore be implemented by simply registering an event handler for these custom events. If an action or function is bound to an entity, the entity also needs to be specified while registering the event handler. In case the action is unbound, the entity can simply be omitted. It’s best practice that the On phase is used to register the event handler. The implementation of the event handler needs to take care of properly completing the event processing.

When implementing a bound action or function a CqnSelect statement is available through the event context, which can be used to select the entity, that was targeted by the action or function. All input parameters and the return value can be accessed and set through the event context. It’s recommended to create a custom event context interface for the action or function to enable type-safe access to the input parameters and the return value.

The following example shows how an action event handler can be implemented:

CDS Model:

service CatalogService {
    entity Books {
        key ID: UUID;
        title: String;
    } actions {
      action review(stars: Integer) returns Reviews;
    };

    entity Reviews {
        book : Association to Books;
        stars: Integer;
    }
}

Custom Event Context:

@EventName("review")
public interface ReviewEventContext extends EventContext {

    // CqnSelect that points to the entity the action was called on
    CqnSelect getCqn();
    void setCqn(CqnSelect select);

    // The 'stars' input parameter
    Integer getStars();
    void setStars(Integer stars);

    // The return value
    void setResult(Reviews review);
    Reviews getResult();

}

Event Handler:

@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {

    @On(event = "review", entity = Books_.CDS_NAME)
    public void reviewAction(ReviewEventContext context) {
        CqnSelect selectBook = context.getCqn();
        Integer stars = context.getStars();
        Reviews review = [...] // create the review
        context.setResult(review);
    }

}

The unused methods setCqn(CqnSelect), setStars(Integer), and getResult() are useful when triggering the event through the service consumption API.

Handling SAP Fiori Drafts

See Cookbook > Draft-based Editing for an overview on SAP Fiori Draft support in CAP.

Editing Drafts

When working with draft-enabled entities, specific draft events are used to interact with entities in draft mode. The following draft-specific events are available:

Request sent by Fiori frontend Event name Default implementation
POST DRAFT_NEW Creates a new empty draft
PATCH with key IsActiveEntity=false DRAFT_PATCH Updates an existing draft
DELETE with key IsActiveEntity=false DRAFT_CANCEL Deletes an existing draft
DELETE with key IsActiveEntity=true DELETE Deletes an active entity and the corresponding draft
POST with action draftPrepare draftPrepare Empty implementation
POST with action draftEdit draftEdit Creates a new draft from an active entity
POST with action draftActivate draftActivate Activates a draft and updates the active entity

You can register @Before handlers for these events, for example to validate incoming data. Note, that instead of using these event names directly, you can use static constants for these events defined in the DraftService interface, which also defines the Consumption API of these events.

See Bookshop sample application for an example.

Activating Drafts

When activating a draft, either a single CREATE or UPDATE event is triggered to create or respectively update the active entity with all of its compositions through a deeply structured document. You can register to these events to validate the activated data.

After activation, a draft is deleted from the database. When deleting active entities explicitly, the DRAFT_CANCEL event is also triggered to delete respective draft entities, corresponding to the active entities. The READ event combines reading drafts and active entities.

Bypassing the SAP Fiori Draft Flow

It’s possible to create and update data directly without creating intermediate drafts. For example, this is useful when prefilling draft-enabled entities with data or in general, when technical components deal with the API exposed by draft-enabled entities. To achieve this, use the following requests. You can register event handlers for the corresponding events to validate incoming data:

HTTP / OData request Event name Default implementation
PUT with key IsActiveEntity=true CREATE / UPDATE Creates or updates the active data (full update)
PATCH with key IsActiveEntity=true CREATE / UPDATE Creates or updates the active data with (sparse update)

These events have the same semantics as described in section Event Flow.

Garbage Collecting Drafts

Inactive drafts are automatically deleted after a timeout (30 days default). You can configure the timeout with the following application configuration property:

cds.drafts.deletionTimeout: 8w

In this example, the draft timeout is set to 8 weeks.

This feature can be also turned off completely by setting the application configuration:

cds.drafts.gc.enabled: false

Before a draft is deleted, the following event is raised:

Event Default implementation
DRAFT_GC Called when a draft times out. Deletes a timed out draft.

You can register a @Before handler to prevent that the draft is deleted by throwing setting the result to false.

Overriding SAP Fiori’s Draft Creation Behaviour

By default SAP Fiori triggers a POST request with an empty body to the entity collection to create a new draft. This behavior can be overridden by implementing a custom action, which SAP Fiori will trigger instead.

  1. Define an action, which is bound to the draft-enabled entity, and annotate it with @cds.odata.bindingparameter.collection.

    The action, that is used to create a new draft needs to be bound to the collection of the draft-enabled entity.

  2. Annotate the draft-enabled entity with @Common.DraftRoot.NewAction: '<action name>'.

    This indicates to SAP Fiori that this action should be used when creating a new draft.

  3. Implement the action in Java.

    The implementation of the action must trigger the newDraft(CqnInsert) method of the DraftService interface to create the draft. In addition, it must return the created draft entity.

The following code summarizes all of these steps in an example:

service AdminService {
  @odata.draft.enabled
  @Common.DraftRoot.NewAction: 'AdminService.createDraft'
  entity Orders as projection on my.Orders actions {
    @cds.odata.bindingparameter.collection
    action createDraft(orderNo: String) returns Orders;
  };
}
@On(entity = Orders_.CDS_NAME)
public void createDraft(CreateDraftContext context) {
    Orders order = Orders.create();
    order.setOrderNo(context.getOrderNo());
    context.setResult(adminService.newDraft(Insert.into(Orders_.class).entry(order)).single(Orders.class));
}

Consuming Services

Most of the time, the implementation of the event handler needs to use other services. Not only CDS services defined in your model are considered services. In CAP also, for example, the database is consumed through a service interface, which understands CQN. For more details about services, see Service Consumption API.

Access to the local API representation of these services can be gained through the ServiceCatalog interface, which is available through the event context. CDS services can be obtained by specifying the fully qualified name as defined in the CDS model. Technical services usually have a default name, that is defined as part of the service’s interface. An example for the name of a technical service is the DEFAULT_NAME constant defined on the PersistenceService interface.

@Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
public void beforeCreateBooks(EventContext context) {
    ServiceCatalog catalog = context.getServiceCatalog();
    PersistenceService db = catalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME);
    CdsService adminService = catalog.getService(CdsService.class, AdminService_.CDS_NAME);
    // use these services
}

When running in Spring, all services are available as Spring beans. Dependency injection can therefore be used to get access to the service objects:

@Component
public class EventHandlerClass implements EventHandler {

    @Autowired
    private PersistenceService db;

    @Autowired
    @Qualifier(AdminService_.CDS_NAME)
    private CdsService adminService;

    @Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
    public void beforeCreateBooks(EventContext context) {
        // use these services
    }

}

Accessing the CDS Model

CAP offers a Model Reflection API to introspect the CDS model of an application and retrieve details on the services, types, entities, and their elements. The model is available through the event context:

@Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
public void beforeCreateBooks(EventContext context) {
    CdsModel model = context.getModel();
    // use the model
}

When running in Spring, the model is available as a Spring bean. It can therefore be accessed through dependency injection:

@Component
public class EventHandlerClass implements EventHandler {

    @Autowired
    private CdsModel model;

    @Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
    public void beforeCreateBooks(EventContext context) {
        // use the model
    }

}

Transaction Handling

By default all processing of an event happens in a transaction. When implementing an event handler, it’s therefore guaranteed that the code is running as part of an existing transaction. If the application doesn’t have any special requirements, no transaction management needs to be performed at all. Multiple calls to insert data to the database through the persistence service are combined into the active transaction automatically. If not otherwise controlled, the transaction is closed as soon as the outermost event is finished processing.

The CAP Java SDK provides an abstraction around transactions, called changeset context.

The changeset context can be used to get more fine-grained control over how events are combined in transactions.

The currently active ChangeSetContext is available through the event context. In case an event handler needs to perform some action shortly before the transaction will be committed or after the transaction was committed or rolled-back, it can register a listener on the changeset context. The ChangeSetListener interface can be used for this case. It allows to register a listener, which is executed shortly before the changeset is closed (beforeClose()) or one, that is executed after the changeset was closed (afterClose(boolean)). The afterClose method has a boolean parameter, which indicates if the changeset was completed successfully (true) or failed and rolled-back (false).

ChangeSetContext changeSet = context.getChangeSetContext();
changeSet.register(new ChangeSetListener() {

    @Override
    public void beforeClose() {
        // do something before changeset is closed
    }

    @Override
    public void afterClose(boolean completed) {
        // do something after changeset is closed
    }

});

The changeset context can also be used to cancel a changeset without throwing an exception. All events in the changeset are processed in that case, but the transaction is rolled back at the end. A changeset can still be canceled from the beforeClose() listener method.

ChangeSetContext changeSet = context.getChangeSetContext();
// cancel changeset without throwing an exception
changeSet.markForCancel();

Database transactions in CAP are always started and initialized lazily, during the first interaction with the persistence service. When running in Spring Boot, CAP Java completely integrates with Spring’s transaction management. As a result you can use Spring’s @Transactional annotations or the TransactionTemplate to control transactional boundaries. This transaction management also comes in handy, in case you need to perform plain JDBC connections in your event handlers. It might be necessary, when calling SAP HANA procedures or selecting from tables not covered by CDS and the persistence service. When annotating an event handler with @Transactional, Spring ensures that a transaction is initialized. CAP in that case ensures, that this transaction is managed as part of an existing changeset context, for which the transaction wasn’t yet initialized. If no such changeset context exists, a new changeset context is created. In case the transaction propagation is specified as REQUIRES_NEW, Spring, and CAP ensure that a new transaction and changeset context are initialized. This mechanism suspends existing transactions and changeset contexts, until the newly created one is closed.

Spring’s transaction management can therefore be used to control transactional boundaries and to initialize transactions more eagerly than CAP. This can be combined with Spring’s standard capabilities to get access to a plain JDBC connection:

@Autowired
private JdbcTemplate jdbc;

@Autowired
private DataSource ds;

@Before(event = CdsService.EVENT_CREATE, entity = Books_.CDS_NAME)
@Transactional // ensure transaction is initialized
public void beforeCreateBooks(List<Books> books) {
    // JDBC template
    jdbc.queryForList("SELECT 1 FROM DUMMY");

    // Connection object
    Connection conn = DataSourceUtils.getConnection(ds);
    conn.prepareCall("SELECT 1 FROM DUMMY").executeQuery();
}

Request Parameters and Users

When events of CAP services are processed in event handler methods the following data are relevant for result compilation:

  • Business data stored in a EventContext object
  • Metadata, as the locale, query parameters as well as properties of the authenticated user

For instance, the user’s logon name is required by the generic handler to fill the CreatedBy column of entities with managed aspect. And the locale of the request is used by the PersistenceService to deliver localized text.

The runtime manages and exposes request-dependent information by means of RequestContext instances, which define a scope that is typically determined by the context of a single request. EventContext is passed along with the event. RequestContext and ChangeSetContext can be accessed through EventContext, but are managed independently. It’s guaranteed, that each CAP service event is executed within the scope of a dedicated RequestContext. How to access the exposed information is described in detail in Reading Request Contexts.

Usually, the protocol adapter opens a single RequestContext that makes the request’s parameters available to CAP services used during request processing. In contrast an OData, $batch request sequentially opens different RequestContexts with divergent parameters for the batch items. In general, it’s possible to explicitly define (nested) RequestContexts and their scope of validity. All CAP services triggered within the same context also share the same parameters. This is described in detail in Defining Request Contexts.

How to propagate RequestContext instances to several threads is explained in Passing Request Contexts to Threads, and Registering Global Parameter Providers shows how you can influence or even override the standard way of retrieving the parameters from the request.

Reading Request Contexts

During processing a service event, the current RequestContext provides information about the request parameters as well as the user:

  • UserInfo exposes an API to fetch data of the (authenticated) user such as the logon name, id, CAP roles, and the tenant.
  • ParameterInfo exposes an API to retrieve additional request data such as header values, query parameters, and the locale.

You can get an instance of UserInfo and ParameterInfo from the EventContext (if provided as handler input parameter) as demonstrated in the following code snippet:

@Before(event = CdsService.EVENT_READ)
public void beforeRead(CdsReadEventContext context) {
    UserInfo userInfo = context.getUserInfo();
    boolean isAuthenticated = userInfo.isAuthenticated();

    ParameterInfo parameterInfo = context.getParameterInfo();
    Instant validFrom = parameterInfo.getValidFrom();
    // ...
}

By means of Spring, both interfaces can be injected:

@Autowired
UserInfo userInfo;

@Autowired
ParameterInfo parameterInfo;

@Before(...)
public void before() {
	boolean isAuthenticated = userInfo.isAuthenticated();
	Instant validFrom = parameterInfo.getValidFrom();
    // ...
}

UserInfo reflects the minimal API, which is required by generic CAP handlers (for example, for authorization). Depending on the configured authentication strategy, a lot more useful information might be presented in the user claim. For instance, XSUAA users additionally bear email, given, and family name etc. You can retrieve these properties with userInfo.getAdditionalAttribute("<property-name>"). To establish type-safe access, additional attributes may also be accessed via custom extensions of UserInfo. To map XSUAA users, interface XsuaaUserInfo is available by default. You can create XsuaaUserInfo instances either by calling userInfo.as(XsuaaUserInfo.class) or by Spring injection:

@Autowired
XsuaaUserInfo xsuaaUserInfo;

@Before(...)
public void before() {
	boolean isAuthenticated = xsuaaUserInfo.isAuthenticated();
	String email = xsuaaUserInfo.getEmail();
	String givenName = xsuaaUserInfo.getGivenName();
	String familyName = xsuaaUserInfo.getFamilyName();
    // ...
}

The same functionality is provided for arbitrary custom interfaces, which are extensions of UserInfo.

Defining Request Contexts

The CAP Java SDK allows you to create new RequestContexts and define their scope. This helps you to control, which set of parameters is consumed by the handlers during service calls. To manually add, modify or reset specific attributes within the scope of a new RequestContext, you can use the RequestContextRunner API:

// read unlocalized books from db - callable from handler code
List<Books> readBooksNotLocalized(EventContext context) {
	// context.getParameterInfo().getLocale() might be set here and
	// persistenceSerive.run would deliver localized data
	return context.getCdsRuntime().requestContext().modifyParameters(param -> param.setLocale(null)).run(newContext -> {
		assert newContext.getParameterInfo().getLocale() == null;
		return persistenceService.run(Select.from(Books_.class)).listOf(Books.class);
	});
}

In the example, a service call to PersistenceService needs to run in a RequestContext without locale in order to retrieve unlocalized data. To achieve this, a new RequestContext is explicitly created by calling requestContext() on the current CdsRuntime. The code being executed in the passed java.util.Function (for example, containing service calls) is triggered with the run() method. Before the execution, the newly created context that wraps the functional code, can be modified arbitrarily:

  • modifyParameters() allows to add, modify, or remove (single) parameters.
  • clearParameters() resets all parameters.
  • providedParameters() resets the parameters according to the registered ParameterInfoProvider (see details below).

Similarly, it’s possible to fully control the UserInfo instance provided in the RequestContext. It’s guaranteed, that the original parameters aren’t touched by the nested RequestContext. In addition, all original parameter values, which aren’t removed or modified are visible in the nested scope. This enables you to either define the parameters from scratch or just to put a modification layer on top.

Some more examples:

  • modifyUser(user -> user.removeRole("read").setTenant(null).run(...) creates a context with a user similar to the outer context but without role read and tenant.
  • clearUser().run(...) runs with anonymous (cleared) user.
  • modifyParamters(param -> param.setHeader("MY-HEADER", "my value")) adds a header parameter MY-HEADER:my value.

The modifications can be combined arbitrarily in fluent syntax.

Passing Request Contexts to Threads

CAP service calls can be executed in different threads. In most cases the RequestContext from the parent thread - typically the worker thread executing the request - needs to be propagated to one or more child threads. Otherwise, required parameter and user information might be missing, for example, when authorizing service calls or creating tenant-specific database connections.

To propagate the parent context, create an instance of RequestContextRunner in the parent thread and open a new RequestContext with run() method in the child thread. This way all parameters from the parent context are also available in the context of one or more spawned threads, as demonstrated in following example:

Locale locale = RequestContext.getCurrent(runtime).getParamterInfo().getLocale();
RequestContextRunner runner = runtime.requestContext();
Future<Result> result = Executors.newSingleThreadExecutor().submit(() -> {
	return runner.run(threadContext -> {
		assert threadContext.getParamterInfo().getLocale().equals(locale);
		return persistenceSerive.run(Select.from(Books_.class));
	});
});

You’re free to modify the parameters by means of the API described in Defining RequestContexts in addition. But please be ware that providedParameters() resp. providedUser() might lead to unexpected behavior as typically the standard providers require to run in the context of the original worker thread to access request-local data.

Registering Global Parameter Providers

The CAP Java runtime ensures that each RequestContext provides a valid instance of UserInfo as well as of ParameterInfo. Hence, if a service is called outside the scope of a context, the runtime has to create a temporary context wrapping this service call. To accomplish this automatic RequestContext creation, the runtime can use the provider APIs, UserInfoProvider resp. ParameterInfoProvider. The default providers registered to the CsdRuntime derive the required information from the HTTP request, if available. Hence, HTTP/REST-based adapters can send service events without spawning a RequestContext explicitly.

The runtime’s provider interface allows customization, that means, the way how UserInfo or ParameterInfo are resolved can be modified or replaced. For example, in Deploy with Confidence (DwC) scenarios with mutual TLS (and thus without XSUAA integration), the user information can’t be derived from a principal attached to the current thread as done in the default UserInfoProvider. Authentication is done outside the service and user information is passed via dedicated header parameters. A custom provider to support this could look like in this sketch:

@Component
@Order(1)
public class DwcUserInfoProvider implements UserInfoProvider {
    @Autowired
    DwcHeaderFacade dwcHeaderFacade; // accesses DwC information type-safely

    @Override
    public UserInfo get() {
    	return UserInfo.create()
        	.setTenant(dwcHeaderFacade.getTenant())
        	.setName(dwcHeaderFacade.getUserName())
        	.setRoles(dwcHeaderFacade.getScopes());
    }
}

It’s allowed to define several providers of the same type. In Spring the provider with the lowest @Order will be called first (in plain Java the order is given by registration order). You can reuse the provider with lower priority and build a modified result. To accomplish this, remember the previous provider instance, which is passed during registration via setPrevious() method call. Such a chain of providers can be used to normalize user names or adjust user roles to match specific needs.

Indicating Errors

The CAP Java SDK provides two different ways to indicate errors:

  • By throwing an exception: This completely aborts the event processing and rollbacks the transaction.
  • By using the Messages API: This adds errors, warnings, info, or success messages to the currently processed request, but doesn’t affect the event processing or the transaction.

The message texts for both exceptions and the Messages API can use formatting and localization.

Exceptions

Any exception that is thrown by an event handler method aborts the processing of the current event and causes any active transaction to be rolled back. To indicate further details about the error, such as a suggested mapping to an HTTP response code, the CAP Java SDK provides a generic unchecked exception class, called ServiceException. It’s recommended to use this exception class, when throwing an exception in an event handler.

When creating a new instance of ServiceException you can specify an ErrorStatus object, through which an internal error code and a mapping to an HTTP status code can be indicated. An enum ErrorStatuses exists, which lists many useful HTTP error codes already. If no such error status is set when creating the ServiceException, it defaults to an internal server error (HTTP status code 500).

// default error status
throw new ServiceException("An internal server error occurred", originalException);
// specifying an error status
throw new ServiceException(ErrorStatuses.CONFLICT, "Not enough stock available")
// specifying an error status and the original exception
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No book title specified", originalException);

The OData V4 adapter turns all exceptions into an OData error response to indicate the error to the client.

Messages

The Messages API allows event handlers to add errors, warnings, info, or success messages to the currently processed request. Adding messages doesn’t affect the event processing or the transaction. This can, for example, be useful to indicate validation errors in a draft entity to the client, while still allowing the invalid draft state to be saved.

The Messages interface provides a logger-like API to collect these messages. Additional optional details can be added to the Message using a builder API. You can access the Messages API from the EventContext. In Spring, you can also autowire it into your event handler class.

@Autowired
Messages messages;

messages.warn("No book title specified");
// adding additional optional details
messages.error("The book is no longer available").code("BNA").longTextUrl("/help/book-not-available");

EventContext context = ...;
context.getMessages().success("The order was sucessfully placed");

The OData V4 adapter collects these messages and writes them into the sap-messages HTTP header by default. However, when an OData V4 error response is returned, because the request was aborted by an exception, the messages are instead written into the details section of the error response. Writing the messages into explicitly modeled messages properties isn’t yet supported.

SAP Fiori uses these messages to display detailed information on the UI. The style how a message appears on the UI depends on the severity of the message.

Formatting and Localization

Texts passed to both ServiceException and the Messages API can be formatted and localized. By default you can use SLF4J’s messaging formatting style to format strings passed to both APIs.

// message with placeholders
messages.warn("Can't order {} books: Not enough on stock", orderAmount);
// on ServiceException last argument can always be the causing exception
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid number: '{}'", wrongNumber, originalException);

You can localize these strings, by putting them into property files and passing the key of the message from the properties file to the API instead of the message text.

When running your application on Spring, the CAP Java SDK integrates with Spring’s support for handling text resource bundles. This handling by default expects translated texts in a messages.properties file under src/main/resources.

The texts defined in the resource bundles can be formatted based on the syntax defined by java.text.MessageFormat. When the message or exception text is sent to the client it’s localized using the client’s locale, as described here.

messages.properties

my.message.key = This is a localized message with {0} parameters

messages_de.properties

my.message.key = Das ist ein übersetzter Text mit {0} Parametern
// localized message with placeholders
messages.warn("my.message.key", paramNumber);
// localized message with placeholders and additional exception
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "my.message.key", paramNumber, originalException);

Exporting the Default Messages

As of CAP Java 1.10.0, you can extract the available default messages as a resource bundle file for further processing (for example, translation). Therefore, the delivery artifact cds-services-utils contains a resource bundle cds-messages-template.properties with all available error codes and default messages. Application developers can use this template to customize error messages thrown by the CAP Java runtime in the application.

  1. Download the artifact or get it from the local Maven repository in ~/.m2/repository/com/sap/cds/cds-services-utils/<VERSION>/cds-services-utils-<VERSION>.jar.
  2. Extract the file.
     jar -f cds-services-utils-<VERSION>.jar -x cds-messages-template.properties
    

    Note: <VERSION> is the version of CAP Java you’re using in your project.

  3. Rename the extracted file cds-messages-template.properties appropriately (for example, to cds-messages.properties) and move it to the resource directory of your application.
  4. In your Spring Boot application, you have to register this additional resource bundle accordingly.

Now, you’re able to customize the stack error messages in your application.

With new CAP Java versions, there could be also new or changed error messages in the stack. To identify these changes, export cds-messages-template.properties from the new CAP Java version and compare it with the previous version using a diff tool.

Target

When SAP Fiori interprets messages it can handle an additional target property, which, for example, specifies which element of an entity the message refers to. SAP Fiori can use this information to display the message along the corresponding field on the UI. When specifying messages in the sap-messages HTTP header, SAP Fiori mostly ignores the target value. Therefore, specifying the target can only correctly be used when throwing a ServiceException as SAP Fiori correctly handles the target property in OData V4 error responses.

The CAP Java SDK provides the ability to specify this target either as a plain string, or using a typed API backed by the CDS model.

// plain String
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No book title specified")
    .messageTarget("title");
// typed API
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No author name specified")
    .messageTarget(Books_.class, b -> b.author().name());

Result Builder

Result objects can be constructed with the ResultBuilder.

Use the selectedRows or insertedRows method for query and insert results, with the data given as Map or list of maps:

import static java.util.Arrays.asList;
import static com.sap.cds.ResultBuilder.selectedRows;

Map<String, Object> row = new HashMap<>();
row.put("title", "Capire");
Result res = selectedRows(asList(row)).result();
context.setResult(res);   // CdsReadEventContext

For query results, the inline count can be set through the inlineCount method:

Result r = selectedRows(asList(row)).inlineCount(inlineCount).result();

For update results, use the updatedRows method with the update count and the update data:

import static com.sap.cds.ResultBuilder.updatedRows;

int updateCount = 1;  // number of updated rows
Map<String, Object> data = new HashMap<>();
data.put("title", "CDS4j");
Result r = updatedRows(updateCount, data).result();

For delete results, use the deletedRows method and provide the number of deleted rows:

import static com.sap.cds.ResultBuilder.deletedRows;

int deleteCount = 7;
Result r = deletedRows(deleteCount).result();

Best Practices and FAQs

This section summarizes some best practices for implementing event handlers and provides answers to frequently asked questions.

  1. On which service should I register my event handler?

    Event handlers implementing business or domain logic should be registered on a CDS service. When implementing rather technical requirements, like triggering some code whenever an entity is written to the database, you can also register event handlers on technical services like the PersistenceService.

  2. Which services should my event handlers usually interact with?

    The CAP Java SDK provides a Service Consumption API that can be used in event handlers to consume other services. These services are consumed to request further data, that is required by the event handler implementation.

    If you’re implementing an event handler of a service, and require additional data of other entities part of that service for validation purposes, it’s a good practice to read this data from the database using the PersistenceService APIs. When using the PersistenceService, no user authentication checks are performed.

    If you’re mashing up your service with another CDS service and also return data from that service to the client, it’s a good practice to consume the other service through its service API. This keeps you decoupled from the possibility that the service might be moved into a dedicated micro-service in the future (late-cut micro services) and automatically lets you consume the business or domain logic of that service. If you want to pass on this decoupling, you can access the service’s entities directly from the database.

    In case you’re working with draft-enabled entities and your event handler requires access to draft states, you should use the DraftService APIs to query and interact with drafts.

  3. How should I implement business or domain logic shared across services?

    In general, it’s a good practice to design your services with specific use cases in mind. Nevertheless, it might be necessary to share certain business or domain logic across multiple services. To achieve this, simple utility methods can be implemented, which can be called from different event handlers.

    If the entities for which a utility method is implemented are different projections of the same database-level entity, you can manually map the entities to the database-level representation and use this to implement your utility method.

    If they’re independent from each other, a suitable self-defined representation needs to be found to implement the utility method.