Skip to content

Commit

Permalink
APP-3110 Handlebars template implementation (finos#260)
Browse files Browse the repository at this point in the history
* APP-3110 Handlebars template engine implementation

* APP-3110 doc

* APP-3110 Post merge, updated getting-started guide

* APP-3110 http and template implementations as runtime dependencies

* APP-3110 reverted ./docs/index.md

* APP-3110 Updates implementation selection

* APP-3110 :symphony-bdk-http-api and :symphony-bdk-template-api as :symphony-bdk-core transitive dependencies
  • Loading branch information
thibauult authored Oct 5, 2020
1 parent 0fa637b commit edb1844
Show file tree
Hide file tree
Showing 29 changed files with 359 additions and 409 deletions.
10 changes: 6 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ If you want to use [Maven](https://maven.apache.org/) as build system, you have
</dependency>
<dependency>
<groupId>com.symphony.platformsolutions</groupId>
<artifactId>symphony-bdk-core-invoker-jersey2</artifactId>
<artifactId>symphony-bdk-http-jersey2</artifactId> <!-- or symphony-bdk-http-webclient -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.symphony.platformsolutions</groupId>
<artifactId>symphony-bdk-template-freemarker</artifactId>
<artifactId>symphony-bdk-template-freemarker</artifactId> <!-- or symphony-bdk-http-handlebars -->
<scope>runtime</scope>
</dependency>
<!-- Logger implementation -->
<dependency>
Expand Down Expand Up @@ -88,8 +90,8 @@ dependencies {
// define dependencies without versions
implementation 'com.symphony.platformsolutions:symphony-bdk-core'
implementation 'com.symphony.platformsolutions:symphony-bdk-core-invoker-jersey2'
implementation 'com.symphony.platformsolutions:symphony-bdk-template-freemarker'
runtimeOnly 'com.symphony.platformsolutions:symphony-bdk-http-jersey2' // or symphony-bdk-http-webclient
runtimeOnly 'com.symphony.platformsolutions:symphony-bdk-template-freemarker' // or symphony-bdk-http-handlebars
implementation 'ch.qos.logback:logback-classic'
}
Expand Down
111 changes: 80 additions & 31 deletions docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,103 @@ More precisely:

## How to use
The central component for the Message API is the `MessageService`.
It exposes all the services mentioned above and is accessible from the `SymphonyBdk` object by calling the `messages()` method.
For instance:

It exposes all the services mentioned above and is accessible from the `SymphonyBdk` object by calling the `messages()` method:
```java
import com.symphony.bdk.core.MessageService;

public class Example {
public static final String STREAM_ID = "gXFV8vN37dNqjojYS_y2wX___o2KxfmUdA";

public static void main(String[] args) throws Exception {
// Create BDK entry point
final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));
// Get the MessageService object
final MessageService messageService = bdk.message();

//send a regular message
final V4Message regularMessage = messageService.send(STREAM_ID, "<messageML>Hello, World!</messageML>");
System.out.println("Message sent, id: " + regularMessage.getMessageId());
// send a regular message
final V4Message regularMessage = bdk.message().send(STREAM_ID, "<messageML>Hello, World!</messageML>");
log.info("Message sent, id: " + regularMessage.getMessageId());
}
}
```

A more detailed example with all exposed services can be found [here](../symphony-bdk-examples/bdk-core-examples/src/main/java/com/symphony/bdk/examples/MessageExampleMain.java).
## Using templates
The `MessageService` also allows you to send messages using templates. So far, the BDK supports two different template
engine implementations:
- [FreeMarker](https://freemarker.apache.org/) (through dependency `com.symphony.platformsolutions:symphony-bdk-template-freemarker`)
- [Handlebars](https://github.com/jknack/handlebars.java) (through dependency `com.symphony.platformsolutions:symphony-bdk-template-handlebars`)

### How to send a message from a template
> In the code examples below, we will assume that FreeMarker as been selected as template engine implementation.
> See [how to select the template engine implementation](#select-your-template-engine-implementation).
## Send messages with templates
The Message service also allows you to send messages using templates. So far, we only support [FreeMarker templates](https://freemarker.apache.org/),
but we may add support for other template engines.
For instance, if you have the following template file:
First you need to define your message template file. Here `src/main/resources/templates/simple.ftl`:
```
<messageML>${message}</messageML>
<messageML>Hello, ${name}!</messageML>
```
you will be able to use it when sending message:
```java
final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));
final V4Message regularMessage = bdk().message().send(streamId, "path/to/template.ftl", Collections.singletonMap("message", "Hello!"));
public class Example {
public static final String STREAM_ID = "gXFV8vN37dNqjojYS_y2wX___o2KxfmUdA";

public static void main(String[] args) {
final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

final V4Message regularMessage = bdk.message().send(streamId, "/templates/simple.ftl", Collections.singletonMap("name", "User"));
}
}
```
The above will send the message `<messageML>Hello, User!</messageML>` as expected.

> Please note that the `MessageService` will try fetch template from different locations ordered by:
> 1. classpath
> 2. file system
It is also possible to get direct access to the `TemplateEngine` through the `MessageService`:
```java
public class Example {

public static void main(String[] args) {
final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

// load template from classpath location
final Template template = bdk.messages().templates().newTemplateFromClasspath("/complex-message.ftl");

// process template with some vars and retrieve content
// any POJO can also be processed by the template
final String content = template.process(Collections.singletonMap("name", "Freemarker"));

// display processed template content
log.info(content);
}
}
```

#### Select your template engine implementation
Developers are free to select the underlying template engine implementation. This can be done importing the right
dependency in your classpath.

With [Maven](./getting-started.md#maven-based-project):
```xml
<dependencies>
<dependency>
<groupId>com.symphony.platformsolutions</groupId>
<artifactId>symphony-bdk-template-freemarker</artifactId>
<scope>runtime</scope>
</dependency>
<!-- or -->
<dependency>
<groupId>com.symphony.platformsolutions</groupId>
<artifactId>symphony-bdk-template-handlebars</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
```
With [Gradle](./getting-started.md#gradle-based-project):
```groovy
dependencies {
runtimeOnly 'com.symphony.platformsolutions:symphony-bdk-template-freemarker'
// or
runtimeOnly 'com.symphony.platformsolutions:symphony-bdk-template-handlebars'
}
```
The above will send the message `<messageML>Hello!</messageML>` as expected.
Please check the [FreeMarker documentation](https://freemarker.apache.org/docs/pgui_quickstart_createdatamodel.html)
to know more about the data model you can use in templates and about the corresponding object structure you need to pass as parameter.

The template name passed as parameter can be the name of a built-in template
(only ["simpleMML"](../symphony-bdk-template/symphony-bdk-template-freemarker/src/main/resources/com/symphony/bdk/template/freemarker/simpleMML.ftl)
available so far), the path to a template file or a URL to a template file.
The templates will be looked for in this order:
* in built-in templates
* in the classpath
* in the file system
* lastly as a URL
> :warning: If multiple implementations found in classpath, an exception is throw in order to help you to define which one
> your project really needs to use.
----
[Home :house:](./index.md)
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ include(':symphony-bdk-http:symphony-bdk-http-webclient')

include(':symphony-bdk-template:symphony-bdk-template-api')
include(':symphony-bdk-template:symphony-bdk-template-freemarker')
include(':symphony-bdk-template:symphony-bdk-template-handlebars')

include(':symphony-bdk-spring:symphony-bdk-core-spring-boot-starter')
include(':symphony-bdk-spring:symphony-bdk-app-spring-boot-starter')

include(':symphony-bdk-examples:bdk-core-examples')
include(':symphony-bdk-examples:bdk-spring-boot-example')
include(':symphony-bdk-examples:bdk-template-examples')

include(':symphony-bdk-legacy:symphony-bdk-bot-sdk-java')
include(':symphony-bdk-legacy:symphony-api-client-java')
Expand Down
4 changes: 2 additions & 2 deletions symphony-bdk-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jacocoTestCoverageVerification {
}

dependencies {
implementation project(':symphony-bdk-http:symphony-bdk-http-api')
implementation project(':symphony-bdk-template:symphony-bdk-template-api')
api project(':symphony-bdk-http:symphony-bdk-http-api')
api project(':symphony-bdk-template:symphony-bdk-template-api')

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

import com.symphony.bdk.http.api.ApiException;

import com.symphony.bdk.template.api.TemplateEngine;

import org.apiguardian.api.API;

/**
Expand All @@ -49,13 +51,15 @@ class ServiceFactory {
private final ApiClient agentClient;
private final AuthSession authSession;
private final BdkConfig config;
private final RetryWithRecoveryBuilder retryBuilder;
private final TemplateEngine templateEngine;
private final RetryWithRecoveryBuilder<?> retryBuilder;

public ServiceFactory(ApiClientFactory apiClientFactory, AuthSession authSession, BdkConfig config) {
this.podClient = apiClientFactory.getPodClient();
this.agentClient = apiClientFactory.getAgentClient();
this.authSession = authSession;
this.config = config;
this.templateEngine = TemplateEngine.getDefaultImplementation();
this.retryBuilder = new RetryWithRecoveryBuilder<>()
.retryConfig(config.getRetry())
.recoveryStrategy(ApiException::isUnauthorized, authSession::refresh);
Expand Down Expand Up @@ -108,8 +112,17 @@ public DatafeedService getDatafeedService() {
* @return an new {@link MessageService} instance.
*/
public MessageService getMessageService() {
return new MessageService(new MessagesApi(agentClient), new MessageApi(podClient),
new MessageSuppressionApi(podClient), new StreamsApi(podClient), new PodApi(podClient),
new AttachmentsApi(agentClient), new DefaultApi(podClient), authSession, retryBuilder);
return new MessageService(
new MessagesApi(this.agentClient),
new MessageApi(this.podClient),
new MessageSuppressionApi(this.podClient),
new StreamsApi(this.podClient),
new PodApi(this.podClient),
new AttachmentsApi(this.agentClient),
new DefaultApi(this.podClient),
this.authSession,
this.templateEngine,
this.retryBuilder
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,44 @@ public class MessageService {
private final AttachmentsApi attachmentsApi;
private final DefaultApi defaultApi;
private final AuthSession authSession;
private final TemplateEngine templateEngine;
private final TemplateResolver templateResolver;
private final RetryWithRecoveryBuilder retryBuilder;

public MessageService(MessagesApi messagesApi, MessageApi messageApi, MessageSuppressionApi messageSuppressionApi,
StreamsApi streamsApi, PodApi podApi, AttachmentsApi attachmentsApi, DefaultApi defaultApi,
AuthSession authSession, RetryWithRecoveryBuilder retryBuilder) {
this(messagesApi, messageApi, messageSuppressionApi, streamsApi, podApi, attachmentsApi, defaultApi, authSession,
new TemplateResolver(), retryBuilder);
}

public MessageService(MessagesApi messagesApi, MessageApi messageApi, MessageSuppressionApi messageSuppressionApi,
StreamsApi streamsApi, PodApi podApi, AttachmentsApi attachmentsApi, DefaultApi defaultApi,
AuthSession authSession, TemplateEngine templateEngine, RetryWithRecoveryBuilder retryBuilder) {
this(messagesApi, messageApi, messageSuppressionApi, streamsApi, podApi, attachmentsApi, defaultApi, authSession,
new TemplateResolver(templateEngine), retryBuilder);
}

private MessageService(MessagesApi messagesApi, MessageApi messageApi, MessageSuppressionApi messageSuppressionApi,
StreamsApi streamsApi, PodApi podApi, AttachmentsApi attachmentsApi, DefaultApi defaultApi,
AuthSession authSession, TemplateResolver templateResolver, RetryWithRecoveryBuilder retryBuilder) {
private final RetryWithRecoveryBuilder<?> retryBuilder;

public MessageService(
final MessagesApi messagesApi,
final MessageApi messageApi,
final MessageSuppressionApi messageSuppressionApi,
final StreamsApi streamsApi,
final PodApi podApi,
final AttachmentsApi attachmentsApi,
final DefaultApi defaultApi,
final AuthSession authSession,
final TemplateEngine templateEngine,
final RetryWithRecoveryBuilder<?> retryBuilder
) {
this.messagesApi = messagesApi;
this.messageApi = messageApi;
this.messageSuppressionApi = messageSuppressionApi;
this.streamsApi = streamsApi;
this.podApi = podApi;
this.attachmentsApi = attachmentsApi;
this.authSession = authSession;
this.templateResolver = templateResolver;
this.templateEngine = templateEngine;
this.templateResolver = new TemplateResolver(templateEngine);
this.defaultApi = defaultApi;
this.retryBuilder = retryBuilder;
}

/**
* Returns the {@link TemplateEngine} that can be used to load templates from classpath or file system.
*
* @return the template engine
*/
public TemplateEngine templates() {
return templateEngine;
}

/**
* Get messages from an existing stream. Additionally returns any attachments associated with the message.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.symphony.bdk.http.api.ApiClient;
import com.symphony.bdk.http.api.ApiException;
import com.symphony.bdk.http.api.ApiRuntimeException;
import com.symphony.bdk.core.auth.AuthSession;
import com.symphony.bdk.core.config.model.BdkRetryConfig;
import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder;
import com.symphony.bdk.core.service.stream.constant.AttachmentSort;
import com.symphony.bdk.core.test.JsonHelper;
Expand All @@ -40,6 +36,9 @@
import com.symphony.bdk.gen.api.model.V4ImportResponse;
import com.symphony.bdk.gen.api.model.V4Message;
import com.symphony.bdk.gen.api.model.V4Stream;
import com.symphony.bdk.http.api.ApiClient;
import com.symphony.bdk.http.api.ApiException;
import com.symphony.bdk.http.api.ApiRuntimeException;
import com.symphony.bdk.template.api.TemplateEngine;
import com.symphony.bdk.template.api.TemplateException;

Expand Down Expand Up @@ -169,8 +168,7 @@ void testSendWithStreamObjectCallsSendWithStreamId() {

@Test
void testSendWithTemplateAndStreamObjectCallsSendWithCorrectStreamIdAndMessage() throws TemplateException {
when(templateEngine.getBuiltInTemplates()).thenReturn(Collections.singleton(TEMPLATE_NAME));
when(templateEngine.newBuiltInTemplate(eq(TEMPLATE_NAME))).thenReturn(parameters -> MESSAGE);
when(templateEngine.newTemplateFromClasspath(eq(TEMPLATE_NAME))).thenReturn(parameters -> MESSAGE);

MessageService service = spy(messageService);
doReturn(new V4Message()).when(service).send(anyString(), anyString());
Expand All @@ -183,8 +181,7 @@ void testSendWithTemplateAndStreamObjectCallsSendWithCorrectStreamIdAndMessage()

@Test
void testSendWithTemplateCallsSendWithCorrectMessage() throws TemplateException {
when(templateEngine.getBuiltInTemplates()).thenReturn(Collections.singleton(TEMPLATE_NAME));
when(templateEngine.newBuiltInTemplate(eq(TEMPLATE_NAME))).thenReturn(parameters -> MESSAGE);
when(templateEngine.newTemplateFromClasspath(eq(TEMPLATE_NAME))).thenReturn(parameters -> MESSAGE);

MessageService service = spy(messageService);
doReturn(new V4Message()).when(service).send(anyString(), anyString());
Expand All @@ -195,8 +192,7 @@ void testSendWithTemplateCallsSendWithCorrectMessage() throws TemplateException

@Test
void testSendWithTemplateThrowingTemplateException() throws TemplateException {
when(templateEngine.getBuiltInTemplates()).thenReturn(Collections.singleton(TEMPLATE_NAME));
when(templateEngine.newBuiltInTemplate(eq(TEMPLATE_NAME))).thenThrow(new TemplateException("error"));
when(templateEngine.newTemplateFromClasspath(eq(TEMPLATE_NAME))).thenThrow(new TemplateException("error"));

assertThrows(TemplateException.class, () -> messageService.send(STREAM_ID, TEMPLATE_NAME, Collections.emptyMap()));
}
Expand Down
14 changes: 6 additions & 8 deletions symphony-bdk-examples/bdk-core-examples/build.gradle
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
description = 'Symphony Java BDK Examples - Core'

dependencies {

implementation project(':symphony-bdk-core')
implementation project(':symphony-bdk-template:symphony-bdk-template-api')
implementation project(':symphony-bdk-http:symphony-bdk-http-api')
implementation project(':symphony-bdk-http:symphony-bdk-http-webclient')
implementation project(':symphony-bdk-template:symphony-bdk-template-freemarker')

implementation 'org.slf4j:slf4j-api'
runtimeOnly project(':symphony-bdk-http:symphony-bdk-http-webclient')
runtimeOnly project(':symphony-bdk-template:symphony-bdk-template-freemarker')

runtimeOnly 'ch.qos.logback:logback-classic'

implementation 'org.slf4j:slf4j-api'
implementation 'commons-io:commons-io'
implementation 'org.apache.commons:commons-lang3'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}

uploadArchives.enabled = false
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ public static void main(String[] args) throws Exception {
final V4Message regularMessage = bdk.messages().send(STREAM_ID, MESSAGE);

//use templates
final V4Message sentWithBuiltInTemplate = bdk.messages().send(STREAM_ID, "simpleMML",
Collections.singletonMap("message", "Hello from a built-in template!"));

final V4Message sendWithCustomTemplate = bdk.messages()
.send(STREAM_ID, "customTemplate.ftl", Collections.emptyMap());
.send(STREAM_ID, "/complex-message.ftl", Collections.singletonMap("name", "Freemarker"));

//retrieve the details of existing messages
final V4Message message = bdk.messages().getMessage(regularMessage.getMessageId());
Expand Down
Loading

0 comments on commit edb1844

Please sign in to comment.