SIP Declarative Adapter

Description

SIP Declarative Adapter concept relies on using best practices in order to provide developers with a tool for building unified adapters. It is a structured approach for building integration adapters. By using predefined SIP constructs (Connector, Integration scenario, Connector Group, Composite process) the adapter development is streamlined and built by the SIP Framework.

Concepts

Common Domain Model (CDM)

CDM is a model which is used in connecting different non-compatible systems. It is also referred to as Canonical Data Model in the standard integration patterns. It is the shared data model that needs to be understood by all Connectors participating in an Integration scenario. Each Integration scenario specifies the CDM that is used for it, while the Connectors participating in the scenario act as interpreters and translate between the CDM and the model used by the external system they are connecting to.

Integration Scenario

A scenario is a means of linking connectors into one fluent flow. They are used to define a specific integration flow, usually one concrete operation, between integration sides. Each adapter contains one or more scenarios that are related to the scope of the adapter itself.

Connector group

Connector groups are used for grouping connectors based on the system they belong to.

Connector

A connector is a holder of an external endpoint and represents one integration side. One connector would typically only talk to multiple external systems if they form a dependent unit - otherwise, for independent systems, separate connectors should be provided, one for each individual system. Their duty is to provide necessary processing and transformation into/from the Common Domain Model. They can be either Inbound or Outbound, with Inbound having Rest and SOAP as pre-built subtypes.

Composite Process

Combines multiple Integration Scenarios into one flow. Its purpose is to allow reuse of the integration scenarios and provide mappings between different CDMs, define order of execution, conditional execution and looping.

Orchestration

SIP Framework allows custom code to be added to multiple predefined points in the adapter that can control the behaviour of the specific declarative element. At the end SIP framework builds the adapter using predefined SIP constructs (mapping, control flow, etc.) and custom code defined by the developer. Custom behaviour supplied by the developer in different places in the adapter is called "orchestration".

There are 3 points of orchestration:

  • Connectors: Behavior of the connector that influences request and (optional) response flow. This orchestration can be written using Connector Processors.
  • Integration scenario: Execution order of Connectors, control flow, and response aggregation. This orchestration can be written in custom SIP Orchestration DSL and Java.
  • Composite process: Execution order of integration scenarios, control flow and mappings. This orchestration can be written in custom SIP Orchestration DSL and Java.

Configuration

To enable or disable declarative adapter building the following configuration property is used:

sip:
  core:
    declarativestructure:
      enabled: true # enabled by default

How to build

Building one adapter requires implementation of previously mentioned concepts.

Integration Scenarios

In order to create a scenario first we need to create a class which extends IntegrationScenarioBase. Then, annotate it with @IntegrationScenario and fill in the required fields:

  • scenarioId - unique identifier of a scenario, used in connectors to link them
  • requestModel - represents the common domain model, that both integration sides communicate through
  • responseModel (optional) - represents the response domain model, which is expected to be returned from outbound side of the flow
  • pathToDocumentationResource (optional) - provides path to scenario documentation files (By default it will look for file in document/structure/integration-scenarios/{scenarioId}.md)
@IntegrationScenario(
        scenarioId = DemoScenario.ID,
        requestModel = DemoCDMRequest.class,
        responseModel = DemoCDMResponse.class)
public class DemoScenario extends IntegrationScenarioBase {
    public static final String ID = "Demo scenario";
}

Connector Groups

Similar to scenarios, we first need to extend ConnectorGroupBase and also annotate the class with @ConnectorGroup. Defining connector group is optional. If a connector group is not represented in code, but is used in a connector, it will be automatically generated by the framework with the ID declared in the connector. Fields that are available are:

  • groupId - unique connector group identifier
  • pathToDocumentationResource (optional) - provides path to documentation files. (By default it will look for file in document/structure/connector-groups/{groupId}.md)
@ConnectorGroup(groupId = DemoConnectorGroup.ID)
public class DemoConnectorGroup extends ConnectorGroupBase {
    public static final String ID = "SIP1";
}

Connectors

Inbound Connectors

Inbound connectors are the entry point to an adapter. They need to extend GenericInboundConnectorBase, or one of the dedicated implementations (REST and SOAP), and override necessary methods. Also, @InboundConnector annotation is required, with the following fields:

  • connectorId (optional) - unique identifier of a connector (automatically generated if missing)
  • connectorGroup - id of the connector group it belongs to
  • integrationScenario - id of the scenario to which it provides data
  • requestModel - model which is expected be received on the input endpoint
  • responseModel (optional) - model which is expected be returned to the caller
  • domains (optional) - list of domains this connector is a part of
  • pathToDocumentationResource (optional) - provides path to documentation files. (By default it will look for file in document/structure/connectors/{connectorId}.md)
Defining the Endpoint

Overloading the method defineInitiatingEndpoint() is used to define the input endpoint.

The most common way to specify the correct EndpointConsumerBuilder expected as the return type for this method, is to use the StaticEndpoindBuilderclass, which contains a large list of integration technologies supported by Apache Camel.

Available options and dependency requirements for each technology can be retrieved from Camel's component reference documentation.

@InboundConnector(
        connectorId = "appendStaticMessageProvider",
        connectorGroup = DemoConnectorGroup.ID,
        integrationScenario = DemoScenario.ID,
        requestModel = InboundConnectorRequest.class,
        responseModel = InboundConnectorResponse.class)
public class DemoConnector extends GenericInboundConnectorBase {

    @Override
    protected EndpointConsumerBuilder defineInitiatingEndpoint() {
        return StaticEndpointBuilders.file("/folder/to/watch");
    }

}
REST Connector

To make the commonly required REST services easier to use, a special base class for inbound connectors is available.

This type of connector easily allows to specify REST services using REST DSL. To do this, adapter developers must override the configureRest(RestDefinition) method, which receives the handle to the DSL as it's sole parameter.

For retrieving and handling path- and query-parameters, the ConnectorProcessor extensions as described in connector orchestration can be utilized. To make the intent of parameter-mapping even more clear, special alias-annotations @ParameterMapping (for @RequestProcessor), as well as @QueryParameter and @PathParameter (for @HeaderParameter) are available.

SIP will automatically expose the OpenAPI specification for REST endpoints created this way, as well as publish a Swagger-UI suite to allow easy testing for these endpoint.

 @InboundConnector(
        connectorGroup = DemoConnectorGroup.ID,
        integrationScenario = RestDSLScenario.ID,
        requestModel = User.class,
        responseModel = UserCreationResult.class)
public class RestConnectorTestBase extends RestConnectorBase {

    @Override
    protected void configureRest(RestDefinition definition) {
        definition
                .post("/user/{userid}")
                .type(User.class)
                .outType(UserCreationResult.class)
                .consumes("application/json");
    }

    @ParameterMapping
    public void attachParameters(User user, @PathParameters("userid") String userId, @QueryParameter("overwriteIfExists") boolean allowOverwrite) {
        user.setId(userid);
    }

}

Outbound Connectors

Outbound connectors are used to define communication with the external systems inside the adapter. They need to extend GenericOutboundConnectorBase and override necessary methods, but also to be annotated with @OutboundConnector.

EndpointProducerBuilder defineOutgoingEndpoint() is used to define the endpoint, which executes the call to an external system. StaticEndpointBuilders can be used to provide the endpoint definition. If the URI of the endpoint contains placeholder values in format of ${placeholder} (value from exchange) or {{placeholder}} (value from configuration) the endpoint will automatically be converted into a dynamic endpoint (toD).

As outbound connectors are otherwise defined identically to inbound ones, additional explanations are available in the documentation for inbound connectors.

Header Cleanup

Some external systems might be sensitive to headers that they receive and don't understand. For example, Camel's REST endpoint automatically sets a lot of headers containing metadata about the incoming request, which would lead to an error within an outbound connector communicating with IBM MQ.

To address this, SIP framework provides the @CleanupHeaders annotation, which can be set on inbound and outbound connectors to temporarily remove any headers that are not explicitly declared to be kept within that annotation.

  • If the annotation is used on an inbound connector, headers will be removed before the message is passed from the inbound connector to the integration scenario, and then re-added for the response message as it is received from the integration scenario. In other words, the headers removed in an inbound connector will be usable within the scope of that connector only, and are not visible to any other connector or orchestration involved in the integration flow.
  • If the annotation is used on an outbound connector, headers will be removed just before the external endpoint is called, and re-added as the response has been received from the endpoint. In other words, the headers will remain available for the full message flow within the SIP adapter, and only be removed for the call to the (external) endpoint.

Note that any cleaned header will only be re-added if no header with the same name has been set elsewhere since it's removal, which means existing headers will not be overwritten when the framework attempts to re-attach the original headers that were removed during header cleanup.

The @CleanupHeaders annotation has an attribute keep, which can be used to declare any number of headers that should not be removed as a regular expression.


@InboundConnector(integrationScenario = "api-bridge")
@CleanupHeaders(keep = "^(?!api-secret)$")  // keeps any header except api-secret
public class CleanupRestInboundConnector extends RestInboundConnectorBase {

    @Override
    protected void configureRest(final RestDefinition definition) {
        definition
                .get("/api")             
                .param()
                    .name("api-secret")
                    .type(RestParamType.header)
                    .dataType("string")
                .endParam();
    }

}

@OutboundConnector(integrationScenario = "api-bridge")
@CleanupHeaders(keep = { "token", "^mq-.+$" })  // removes any header except "token" and anything starting with "mq-"
public class CleanupMqOutboundConnector extends GenericOutboundConnectorBase {

    @Override
    protected EndpointProducerBuilder defineOutgoingEndpoint() {
        return StaticEndpointBuilders.jms("...");        
    }

}

Orchestration

Connector Orchestration

Connector orchestration allows to define any number of processing steps that need to be completed within the scope of a connector. Typical tasks include:

  • Mapping between various data models (such as from the system specific model to the common domain model)
  • Attaching data from the message header to the model (such as query- or path-parameters)
  • Verifying information retrieved with a request (such as authorization tokens)
Attaching Connector Processors

SIP allows to implement and attach processors to (inbound- and outbound-) connectors in a variety of ways.

Internally, all processors rely on the ConnectorProcessor interface, but it is not always necessary for adapter developers to implement this interface themselves.

Connector processors are attached to either the request- or the response-flow of a connector, typically using the respective @RequestProcessor and @ResponseProcessor annotations.

If multiple processors are attached to a connector, the ordering in which they are processed can often be important. Chapter Ordering Connector Processors describes how the correct oder can be specified.

Using model mappers

The ModelMapper interface allows to implement mappers that transform data from a source- to a target-model. Typically, they are used to transform from a system-specific data model to the common domain model used in an integration scenario.

A mapper is usually attached to a connector by annotation the connector-class with either @UseRequestModelMapper or @UseResponseModelMapper.

Any class implementing ModelMapper is automatically a ConnectorProcessor as well, and can therefore also be placed in the correct order with any other processors used within the connector.

public class SysUserMapper implements ModelMapper<SystemUser, User> {

    @Override
    public User mapToTargetModel(SystemUser sourceUser) {
        var user = new User();
        // do the mapping
        return user;
    }

}

@InboundConnector(requestModel = SystemUser.class)
@UseRequestModelMapper(SysUserMapper.class)
public class SysUserInboundConnector extends GenericInboundConnectorBase {

    @Override
    protected EndpointConsumerBuilder defineInitiatingEndpoint() {
        return StaticEndpointBuilders.jms("jms:queue:newuser");
    }

}
Annotating methods inside a connector class

Any public method inside a connector's class can be annotated with either @RequestProcessor or @ResponseProcessor. The SIP framework will then automatically attach it to the respective flow of that connector.

Following variants are supported:

  1. The method can take zero parameters and declare a return type that implements ConnectorProcessor. On adapter startup, SIP framework will then call this method once and attach the provided processor to the flow of the connector.

  2. The method does not have a return type (void) and has any number of arguments. SIP will automatically wrap this method into a ConnectorProcessor instance, and will assign the given parameters using the following rules:

    • Parameters of type Message or Exchange will receive the respective instance.
    • Parameters annotated with @HeaderParameter(String) will receive the content of the respectively named header-field in the declared type.
    • Any other parameter will retrieve the current content of the body in the declared type. @Nullable can be added if null be legal for that parameter.
  3. The method does have a return type which is not a ConnectorProcessor instance and any number of arguments. This will behave identically to approach 2, but additionally uses the returned object as the new message body, effectively making it a simple model mapper.

Note that for variants 2 and 3, any necessary type conversions are processed through Camel's type converter API.

@InboundConnector(requestModel = UserRequest.class, responseModel = User.class)
public class UserRequestInboundConnector extends RestConnectorBase {

    @Override
    protected void configureRest(RestDefinition definition) {
        definition
                .get("/user/{userid}")
                .outType(UserCreationResult.class);
    }

    // Variant 1
    @RequestProcessor
    public ConnectorProcessor authorizeRequest() {
        return TokenValidator.INSTANCE;
    }

    // Variant 3
    @RequestProcessor
    public UserRequest mapPathIdToUserRequest(@HeaderParameter("userid") String id) {
        return UserRequest.builder().id(id).build();
    }

    // Variant 2
    @ResponseProcessor
    public void obfuscateVipUserData(User retrievedUser) {
        if (retrievedUser.isVip()) {
            VipObfuscator.INSTANCE.obfuscatePersonalData(retrievedUser);
        }
    }

}

Note that instead of using @RequestProcessor and @HeaderParameter annotations for the parameter mapping (as shown above), there are also @ParameterMapping, @PathParameter, and @QueryParameter annotations available (as described in the REST Connector chapter).

Attaching a processor bean to a connector

It is possible to attach a bean implementing ConnectorProcessor to a connector by referencing the connector's id or class within the @RequestProcessor or @ResponseProcessor annotation. This can be helpful if the connector can or should not be modified.

@Component
@RequestProcessor(ClosedSourceInboundConnector.class)
class MessagePeek implements ConnectorProcessor {

    @Override
    public void process(final Exchange exchange) throws Exception {
        String body = exchange.getMessage().getBody(String.class);
        // do something useful
    }

}
Ordering Connector Processors

Ordering of connector processors can be achieved via specific annotations, which can be applied in combination with all annotations used to attach processors (i.e. @RequestProcessor, @ResponseProcessor, @ParameterMapping, @UseRequestModelMapper, and @UseResponseModelMapper).

Ordering can either be defined using an absolute order (i.e. numbering the connector processor), or relative ordering of the processors in relation to each other.

It is also possible to combine absolute and relative ordering. In this case, the absolute orderings are applied first, and the relative orderings are applied afterward.

The placement of processors without any ordering annotation is non-deterministic.

Absolute ordering

The @ExecutionOrder annotation can be utilized to provide absolute ordering for a connector processor. The annotation supports three variants:

  1. By supplying the order-number of this processor's execution. The processors will be executed in the order from the lowest to the highest number. A continuous numbering is not required. If there are multiple processors with the same ordering number, their execution order is non-deterministic.
  2. By setting @ExecutionOrder(first = true). This processor will always be executed before any other. This may only be specified once per connector.
  3. By setting @ExecutionOrder(last = true). This processor will always be last after any other. This may only be specified once per connector.
@InboundConnector(requestModel = String.class)
@UseRequestModelMapper(StringToCdmMapper.class)
@ExecutionOrder(last = true)
public class StringManipulatingInboundProcessor extends GenericInboundConnectorBase {

    @Override
    protected EndpointConsumerBuilder defineInitiatingEndpoint() {
        return StaticEndpointBuilders.file("/folder/to/watch");
    }

    @ExecutionOrder(first = true)
    public ConnectorProcessor authorizeRequest() { /* ... */ }

    @ExecutionOrder(2)
    public String fileToUpper(String fileContent) { return fileContent.toUpperCase(); }

    @ExecutionOrder(1)
    public void verifyReadable(File file) { if (!file.canRead()) throw new IllegalArgumentException();  }

}

In this example, authorizeRequest will be called first, followed by verifyReadable, fileToUpper and finally the attached StringToCdmMapper instance.

Relative ordering

The @ExecuteBefore and @ExecuteAfter annotations can be utilized to specify ordering of connector processors relative to each other. These annotations expect either the class or processor-name as a pointer to the processor which should be used for the relative ordering. When using method-based connector processors, the name of the method serves as the processor-name that can be used.

Below is the previous example rewritten with relative ordering.

@InboundConnector(requestModel = String.class)
@UseRequestModelMapper(StringToCdmMapper.class)
public class StringManipulatingInboundProcessor extends GenericInboundConnectorBase {

    @Override
    protected EndpointConsumerBuilder defineInitiatingEndpoint() {
        return StaticEndpointBuilders.file("/folder/to/watch");
    }

    @ExecuteBefore(processorName = "verifyReadable")
    public ConnectorProcessor authorizeRequest() { /* ... */ }

    @ExecuteBefore(StringToCdmMapper.class)
    public String fileToUpper(String fileContent) { return request.toUpperCase(); }

    @ExecuteBefore(processorName = "authorizeRequest")
    public void verifyReadable(File file) { if (!file.canRead()) throw new IllegalArgumentException();  }

}
DEPRECATED: Orchestration via defineTransformationOrchestrator()

[!WARNING]
This variant of connector orchestration is deprecated since 3.4.0, and support might be removed in the future. This approach is also mutually exclusive with the Connector Processor features described above, so either one or the other can be used for any connector.

Overriding defineTransformationOrchestrator() in the connector class allows to return a custom Orchestrator to be used with connector.

The typical pattern when overloading this method is to return a ConnectorOrchestrator instance that provides custom request- and/or response-transformers through Camel's RouteDefinition DSL.

@OutboundConnector(requestModel = String.class, responseModel = String.class)
public class StringAppendingOutboundConnector extends GenericOutboundConnectorBase {

    @Override
    protected EndpointProducerBuilder defineOutgoingEndpoint() {
        return StaticEndpointBuilders.https("example.com/rest/endpoint");
    }

    @Override
    protected Orchestrator<ConnectorOrchestrationInfo> defineTransformationOrchestrator() {
        return ConnectorOrchestrator.forConnector(this)
                .setRequestRouteTransformer(this::defineRequestRoute)
                .setResponseRouteTransformer(this::defineResponseRoute);
    }

    protected void defineRequestRoute(final RouteDefinition definition) {
        definition.setBody(exchange -> exchange.getIn().getBody() + "-REQUEST");
    }

    protected void defineResponseRoute(final RouteDefinition definition) {
        definition.setBody(exchange -> exchange.getIn().getBody() + "-RESPONSE");
    }

}

Integration Scenario

The integration scenario is defined with the @IntegrationScenario annotation, which specifies the scenarioId, requestModel, and responseModel. In this example, the scenarioId is set to "Demo scenario," and the request and response models are DemoCDMRequest and DemoCDMResponse respectively.

The DemoScenario class extends IntegrationScenarioBase, indicating that it is part of a framework for defining integration scenarios. The class overrides the getOrchestrator method, which is responsible for setting up the orchestration logic. This method returns an orchestrator configured using the DSL provided by ScenarioOrchestrator.

Within the getOrchestrator method, ScenarioOrchestrator.forOrchestrationDslWithResponse is used to create an orchestrator that handles the orchestration flow with a response. The DSL is used to define the sequence of actions: it specifies that the scenario should start by handling requests from the DemoConnector (an inbound connector) and then proceed to call the DemoOutboundConnector (an outbound connector). The call to andNoResponseHandling() indicates that no additional response handling is required after the outbound connector is called.

This example is simplified and mainly serves to illustrate the use of the DSL for defining an integration scenario. In a real-world application, more complex logic and additional connectors might be involved. However, with only two connectors participating, such detailed orchestration might not be necessary. The primary purpose here is to showcase the structure and capabilities of the DSL in managing integration scenarios

@IntegrationScenario(
        scenarioId = DemoScenario.ID,
        requestModel = DemoCDMRequest.class,
        responseModel = DemoCDMResponse.class)
public class DemoScenario extends IntegrationScenarioBase {

    public static final String ID = "Demo scenario";

    @Override
    public Orchestrator<ScenarioOrchestrationInfo> getOrchestrator() {
        return ScenarioOrchestrator.forOrchestrationDslWithResponse(DemoCDMRequest.class,
                dsl -> dsl.forInboundConnectors(DemoConnector.class)
                        .callOutboundConnector(DemoOutboundConnector.class)
                        .andNoResponseHandling());
    }
}

Following example showcases the use of an integration scenario orchestration, with additional features for request preparation and response handling. In this example you can see that there are 4 participants in this integration scenario (DemoConnector, FirstDemoOutboundConnector, SecondDemoOutboundConnector, ThirdDemoOutboundConnector). There is a simple condition in the beginning that checks the size of an item list.

In case the item list size is greater than 10: - First FirstDemoOutboundConnector is called and the response will not be handled - Afterward SecondDemoOutboundConnector will be called without any response handling

In case the item list size is smaller than 10: - Before calling the third outbound connector, the request can be modified by calling withRequestPreparation(). Even if the method call comes after callOutboundConnector() it is executed before the request is sent. In this example, if the date in the original DemoCDMRequest is before the current time, the estimated delivery date is set to two days from now. - Moreover, the snippet demonstrates response handling, allowing modifications to the response before it is sent back to the initial caller by calling andHandleResponse(). The response handling step accesses the latest step's response and modifies its message to "demo".

These enhancements illustrate the flexibility of the DSL in managing complex integration scenarios, providing hooks to alter the request and response as needed, thereby enabling dynamic and context-aware processing within the orchestration flow. You can even use loops in this orchestration as explained in section Loops.

@IntegrationScenario(
        scenarioId = DemoScenario.ID,
        requestModel = DemoCDMRequest.class,
        responseModel = DemoCDMResponse.class)
public class DemoScenario extends IntegrationScenarioBase {

    public static final String ID = "Demo scenario";

    @Override
    public Orchestrator<ScenarioOrchestrationInfo> getOrchestrator() {
        return ScenarioOrchestrator.forOrchestrationDslWithResponse(DemoCDMRequest.class,
                dsl -> dsl.forInboundConnectors(DemoConnector.class)
                        .ifCase(context -> context.getOriginalRequest(DemoCDMRequest.class).getItems().size() > 10)
                        .callOutboundConnector(FirstDemoOutboundConnector.class)
                        .andNoResponseHandling()
                        .callOutboundConnector(SecondDemoOutboundConnector.class)
                        .andNoResponseHandling()
                        .elseCase()
                        .callOutboundConnector(ThirdDemoOutboundConnector.class)
                        .withRequestPreparation(scenarioOrchestrationContext -> {
                            DemoCDMRequest demoCDMRequest = scenarioOrchestrationContext.getOriginalRequest(DemoCDMRequest.class);
                            if (demoCDMRequest.getDate().isBefore(Instant.now())) {
                                demoCDMRequest.setEstimatedDelivery(Instant.now().plus(Period.ofDays(2)));
                            }
                            return demoCDMRequest;
                        })
                        .andHandleResponse((demoCDMResponse, scenarioOrchestrationContext) -> {
                            scenarioOrchestrationContext.getResponseForLatestStep().ifPresent(demoCDMResponseOrchestrationStepResponse -> {
                                demoCDMResponseOrchestrationStepResponse.result().setMessage("demo");
                            });
                        })
                        .endCases());
    }
}

Composite Processes

Similar procedure should be followed as on the other declarative elements. We first need to extend CompositeProcessBase. Then, annotate the class with @CompositeProcess and fill in the required fields:

  • processId - unique identifier of a process, used to identify it in SIP, should be unique
  • provider - represents the integration scenario that provides the data to the process
  • consumers (array) - represents integration scenarios that consume data from the process
  • pathToDocumentationResource (optional) - provides path to process documentation files (By default it will look for file in document/structure/processes/.md)
@CompositeProcess(processId = "demo-process", consumers = {DemoScenarioConsumer1.class, DemoScenarioConsumer2.class},
        provider = DemoScenario.class)
public class DemoProcess extends CompositeProcessBase {
    public static final String ID = "demo-process";

    @Override
    public Orchestrator<CompositeProcessOrchestrationInfo> getOrchestrator() {
        return ProcessOrchestrator.forOrchestrationDsl(
                dsl -> {
                    dsl.callConsumer(DemoScenarioConsumer1.class)
                            .withRequestPreparation(
                                    context -> {
                                        ResponseModel response = context.<ResponseModel>getLatestResponse()
                                                .orElseThrow(() -> new SIPAdapterException("Invalid response"));
                                        return ResponseModel2.builder()
                                                .field1(response.getValue1())
                                                .field2(response.getValue2())
                                                .build();
                                    })
                            .withResponseHandling(
                                    (latestResponse, context) -> {
                                        log.debug(String.valueOf(latestResponse));
                                    })
                            .callConsumer(DemoScenarioConsumer2.class)
                            .withNoResponseHandling();
                });
    }
}

It is also possible to use conditional statements or loops in process orchestration.

Conditionals
  • ifCase opens the conditional statement in the DSL. It accepts a predicate as a parameter, which needs to be evaluated into a boolean from the given context. Inside of it one or more consumer calls may be defined.
  • elseIfCase may be used after statements from ifCase. It also accepts a predicate as a parameter.
  • elseCase does not take a parameter, it will be executed if the previous conditions from ifCase or elseIfCase are not met.
  • endCases is used to finish the condition statements and return to previous scope.
public class DemoProcess extends CompositeProcessBase {

    @Override
    public Orchestrator<CompositeProcessOrchestrationInfo> getOrchestrator() {
        return ProcessOrchestrator.forOrchestrationDsl(
                dsl -> { dsl
                        .ifCase(hasHeader("headerName"))
                        .callConsumer(DemoScenarioConsumer1.class)
                        .withNoResponseHandling()
                        .elseIfCase(context -> context.getLatestResponse().get().isOk())
                        .callConsumer(DemoScenarioConsumer2.class)
                        .withNoResponseHandling()
                        .elseCase()
                        .callConsumer(DemoScenarioConsumer3.class)
                        .withNoResponseHandling()
                        .endCases();
                });
    }

}
Loops
  • doWhile is used to enter looping statement equivalent to 'while' from DSL. It enables looping until the condition passed as a predicate is no longer true. The same predicates as in ifCase can be used.
    To end the loop 'endDoWhile' should be used, which will return to the previous scope.
  • forLoop is used to enter looping statement equivalent to 'for' from DSL. It accepts a predicate which should be evaluated into an integer marking the number of iterations. To return to previous scope and end the loop endForLoop should be used.
public class DemoProcess extends CompositeProcessBase {

    @Override
    public Orchestrator<CompositeProcessOrchestrationInfo> getOrchestrator() {
        return ProcessOrchestrator.forOrchestrationDsl(
                dsl -> { dsl
                        .doWhile(hasHeader("headerName"))
                        .callConsumer(DemoScenarioConsumer1.class)
                        .withNoResponseHandling()
                        .endDoWhile()
                        .forLoop(context -> context.getLatestResponse().get().getIterationNumber())
                        .callConsumer(DemoScenarioConsumer2.class)
                        .withNoResponseHandling()
                        .endForLoop();
                });
    }

}

Configuration and exception handling

Scenario and connector level handlers

To provide a more fine-grained approach to different types of handlers, the SIP framework offers ways to configure handlers on either scenario or connector level. This approach is based on the existing functionalities provided by Apache Camel, but in a more structured way.

To achieve this, first, a configuration class should be created which implements ConfigurationDefinition interface. The method which must be overridden provides a hook to RouteConfigurationDefinition which offers adding handlers (onException, onCompletion, intercept, interceptFrom, interceptSendToEndpoint). One handler should be defined per class as the return type of the method suggests.

@Configuration
public class SIPAdapterExceptionHandler implements ConfigurationDefinition {

    @Override
    public OutputDefinition define(RouteConfigurationDefinition routeConfigurationDefinition) {
        return routeConfigurationDefinition
                .onException(SIPAdapterException.class, IllegalArgumentException.class)
                .process(exchange -> {
                    String message = exchange
                            .getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class)
                            .getMessage();
                    exchange.getMessage().setBody(message);
                    exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
                })
                .handled(true);
    }
}

Second step would be marking the desired Scenario or Connector to use this handler. This is done via @ConfigurationHandler annotation. It requires that the handler classes are provided as parameters.

When done on scenario level it will apply to all connectors which belong to it.

@IntegrationScenario(
        scenarioId = "scenarioId",
        requestModel = String.class,
        responseModel = String.class)
@ConfigurationHandler(SIPAdapterExceptionHandler.class)
public class DemoScenario extends IntegrationScenarioBase {}

When done on connector level it will apply only to that one connector. Both inbound and outbound may be used.

@InboundConnector(
        connectorGroup = "group1",
        integrationScenario = "scenarioId",
        requestModel = String.class,
        responseModel = String.class)
@ConfigurationHandler(SIPAdapterExceptionHandler.class)
public class DemoConnector extends GenericInboundConnectorBase {...}

Of course the global level handlers may still be created using standard RouteConfigurationBuilder.

Connector level exception handlers

If there is a need for a dedicated exception handler in a connector, this is also possible. To do so, inside the connector a public method must be created with ConnectorOnExceptionDefinition as the return type. The return type is a hook into OnExceptionDefinition from Apache Camel. This method must also be annotated with @ConnectorExceptionHandler inside which the exception types, which should be handled by the method, are declared. It is possible to define multiple methods, each which would handle a different exception in its own way.

@ConnectorExceptionHandler(RuntimeException.class)
public ConnectorOnExceptionDefinition define() {
    return onException ->
            onException
                    .process(doProcessing())
                    .handled(true);
}

These exception handlers will override any other handler, meaning if scenario or connector level configuration handler exists, which handles the same exception type, these handlers will take priority.