Browse Source

TBEL support in notification templates

pull/8568/head
ViacheslavKlimov 4 years ago
parent
commit
51fa46c8be
  1. 3
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  3. 40
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  4. 95
      application/src/main/java/org/thingsboard/server/service/notification/TemplateService.java
  5. 8
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  6. 73
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java
  7. 56
      common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java
  8. 3
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptType.java

3
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -62,6 +62,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.notification.TemplateService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -102,6 +103,7 @@ public class NotificationController extends BaseController {
private final NotificationTargetService notificationTargetService;
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
private final TemplateService templateService;
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
@ -334,6 +336,7 @@ public class NotificationController extends BaseController {
.deliveryMethods(deliveryMethods)
.template(template)
.settings(null)
.templateService(templateService)
.build();
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = ctx.getDeliveryMethods().stream()
.collect(Collectors.toMap(m -> m, deliveryMethod -> {

3
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -95,12 +95,12 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private final NotificationService notificationService;
private final NotificationTemplateService notificationTemplateService;
private final NotificationSettingsService notificationSettingsService;
private final UserService userService;
private final NotificationExecutorService notificationExecutor;
private final DbCallbackExecutorService dbCallbackExecutorService;
private final NotificationsTopicService notificationsTopicService;
private final TbQueueProducerProvider producerProvider;
private final RateLimitService rateLimitService;
private final TemplateService templateService;
private Map<NotificationDeliveryMethod, NotificationChannel> channels;
@ -169,6 +169,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.deliveryMethods(deliveryMethods)
.template(notificationTemplate)
.settings(settings)
.templateService(templateService)
.build();
notificationExecutor.submit(() -> {

40
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.notification;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import lombok.Builder;
import lombok.Getter;
@ -31,12 +32,13 @@ import org.thingsboard.server.common.data.notification.template.HasSubject;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.util.TemplateUtils;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@ -57,9 +59,11 @@ public class NotificationProcessingContext {
@Getter
private final NotificationRequestStats stats;
private final TemplateService templateService;
@Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set<NotificationDeliveryMethod> deliveryMethods,
NotificationTemplate template, NotificationSettings settings) {
NotificationTemplate template, NotificationSettings settings, TemplateService templateService) {
this.tenantId = tenantId;
this.request = request;
this.deliveryMethods = deliveryMethods;
@ -67,14 +71,22 @@ public class NotificationProcessingContext {
this.notificationTemplate = template;
this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
this.stats = new NotificationRequestStats();
this.templateService = templateService;
init();
}
private final Map<String, Function<NotificationRecipient, String>> recipientTemplateParams = Map.of(
"recipientTitle", NotificationRecipient::getTitle,
"recipientEmail", recipient -> Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", recipient -> Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", recipient -> Strings.nullToEmpty(recipient.getLastName())
);
private void init() {
NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration();
templateConfig.getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (template.isEnabled()) {
template = processTemplate(template, null); // processing template with immutable params
template = processTemplate(template, null, recipientTemplateParams.keySet());
templates.put(deliveryMethod, template);
}
});
@ -91,12 +103,12 @@ public class NotificationProcessingContext {
additionalTemplateContext = createTemplateContextForRecipient(recipient);
}
if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) {
template = processTemplate(template, additionalTemplateContext);
template = processTemplate(template, additionalTemplateContext, Collections.emptySet());
}
return template;
}
private <T extends DeliveryMethodNotificationTemplate> T processTemplate(T template, Map<String, String> additionalTemplateContext) {
private <T extends DeliveryMethodNotificationTemplate> T processTemplate(T template, Map<String, String> additionalTemplateContext, Set<String> ignoredParams) {
Map<String, String> templateContext = new HashMap<>();
if (request.getInfo() != null) {
templateContext.putAll(request.getInfo().getTemplateData());
@ -104,35 +116,31 @@ public class NotificationProcessingContext {
if (additionalTemplateContext != null) {
templateContext.putAll(additionalTemplateContext);
}
if (templateContext.isEmpty()) return template;
if (templateContext.isEmpty() && ignoredParams.isEmpty()) return template;
template = (T) template.copy();
template.setBody(TemplateUtils.processTemplate(template.getBody(), templateContext));
template.setBody(templateService.processTemplate(tenantId, template.getBody(), templateContext, ignoredParams));
if (template instanceof HasSubject) {
String subject = ((HasSubject) template).getSubject();
((HasSubject) template).setSubject(TemplateUtils.processTemplate(subject, templateContext));
((HasSubject) template).setSubject(templateService.processTemplate(tenantId, subject, templateContext, ignoredParams));
}
if (template instanceof WebDeliveryMethodNotificationTemplate) {
WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template;
String buttonText = webNotificationTemplate.getButtonText();
if (isNotEmpty(buttonText)) {
webNotificationTemplate.setButtonText(TemplateUtils.processTemplate(buttonText, templateContext));
webNotificationTemplate.setButtonText(templateService.processTemplate(tenantId, buttonText, templateContext, ignoredParams));
}
String buttonLink = webNotificationTemplate.getButtonLink();
if (isNotEmpty(buttonLink)) {
webNotificationTemplate.setButtonLink(TemplateUtils.processTemplate(buttonLink, templateContext));
webNotificationTemplate.setButtonLink(templateService.processTemplate(tenantId, buttonLink, templateContext, ignoredParams));
}
}
return template;
}
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
return Map.of(
"recipientTitle", recipient.getTitle(),
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", Strings.nullToEmpty(recipient.getLastName())
);
return recipientTemplateParams.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().apply(recipient)));
}
}

95
application/src/main/java/org/thingsboard/server/service/notification/TemplateService.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification;
import com.google.common.base.Strings;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import static com.google.common.base.Strings.nullToEmpty;
import static org.apache.commons.lang3.StringUtils.removeStart;
@Service
public class TemplateService {
private static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\$\\{(.+?)(:[a-zA-Z]+)?}");
private static final Pattern TEMPLATE_SCRIPT_PATTERN = Pattern.compile("#\\{(.+?)}");
private static final Map<String, UnaryOperator<String>> SHORT_FUNCTIONS = Map.of(
"upperCase", String::toUpperCase,
"lowerCase", String::toLowerCase,
"capitalize", StringUtils::capitalize
);
@Autowired(required = false)
private TbelInvokeService tbelInvokeService;
public String processTemplate(TenantId tenantId, String template, Map<String, String> context, Set<String> ignoredParams) {
String result = TEMPLATE_PARAM_PATTERN.matcher(template).replaceAll(matchResult -> {
String key = matchResult.group(1);
if (!context.containsKey(key) || ignoredParams.contains(key)) {
return "\\" + matchResult.group();
}
String value = nullToEmpty(context.get(key));
String function = removeStart(matchResult.group(2), ":");
if (function != null) {
if (SHORT_FUNCTIONS.containsKey(function)) {
value = SHORT_FUNCTIONS.get(function).apply(value);
}
}
return value;
});
if (tbelInvokeService != null) {
result = TEMPLATE_SCRIPT_PATTERN.matcher(result).replaceAll(matchResult -> {
String script = matchResult.group(1);
if (!ignoredParams.isEmpty() && ignoredParams.stream().anyMatch(script::contains)) {
return matchResult.group();
}
return processScript(tenantId, script, context);
});
}
return result;
}
@SneakyThrows
private String processScript(TenantId tenantId, String script, Map<String, String> context) {
String scriptBody = "return " + script + ";";
String[] argsNames = context.keySet().toArray(String[]::new);
Object[] args = context.values().stream().map(Strings::nullToEmpty).toArray();
try {
UUID scriptId = tbelInvokeService.eval(tenantId, ScriptType.TEMPLATE_SCRIPT, scriptBody, argsNames).get();
Object result = tbelInvokeService.invokeScript(tenantId, null, scriptId, args).get();
return String.valueOf(result);
} catch (ExecutionException e) {
throw e.getCause();
}
}
}

8
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -128,6 +128,12 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = setUpNotificationTemplate(notificationType, subject, text, deliveryMethods);
return saveNotificationTemplate(notificationTemplate);
}
protected NotificationTemplate setUpNotificationTemplate(NotificationType notificationType, String subject,
String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setTenantId(tenantId);
notificationTemplate.setName("Notification template: " + text);
@ -161,7 +167,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
}
notificationTemplate.setConfiguration(config);
return saveNotificationTemplate(notificationTemplate);
return notificationTemplate;
}
protected NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {

73
application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java

@ -19,27 +19,47 @@ import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class NotificationTemplateApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationCenter notificationCenter;
@Autowired
private NotificationService notificationService;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
@ -77,6 +97,59 @@ public class NotificationTemplateApiTest extends AbstractNotificationApiTest {
.contains("name is malformed");
}
@Test
public void testTemplatesProcessing_tbelScripts() throws Exception {
User firstRecipient = new User();
firstRecipient.setEmail("recipient1@tb.org");
firstRecipient.setAuthority(Authority.TENANT_ADMIN);
firstRecipient.setFirstName("Aba");
firstRecipient.setLastName("Duba");
firstRecipient = createUser(firstRecipient, "123456");
User secondRecipient = new User();
secondRecipient.setEmail("recipient2@tb.org");
secondRecipient.setAuthority(Authority.TENANT_ADMIN);
secondRecipient = createUser(secondRecipient, "123456");
NotificationTargetId target = createNotificationTarget(firstRecipient.getId(), secondRecipient.getId()).getId();
String template = "Hi, #{ recipientFirstName.isEmpty() ? recipientEmail : (recipientFirstName + ' ' + recipientLastName) } #{ '!!!' }";
process(template, NotificationType.GENERAL, null, target);
assertThat(getLastNotification(firstRecipient).getText()).isEqualTo("Hi, Aba Duba !!!");
assertThat(getLastNotification(secondRecipient).getText()).isEqualTo("Hi, recipient2@tb.org !!!");
NotificationInfo info = EntityActionNotificationInfo.builder()
.entityId(new DeviceId(UUID.randomUUID()))
.entityName("device A")
.actionType(ActionType.DELETED)
.userId(tenantAdminUserId.getId())
.build();
template = "Hi, #{ recipientTitle } (${recipientEmail}). Someone ${actionType} your ${entityName}" +
"#{ actionType == 'deleted' ? '!!!' : '.' } #{ ':(' }";
process(template, NotificationType.ENTITY_ACTION, info, target);
assertThat(getLastNotification(firstRecipient).getText()).isEqualTo("Hi, Aba Duba (recipient1@tb.org). " +
"Someone deleted your device A!!! :(");
assertThat(getLastNotification(secondRecipient).getText()).isEqualTo("Hi, recipient2@tb.org (recipient2@tb.org). " +
"Someone deleted your device A!!! :(");
}
private void process(String text, NotificationType notificationType, NotificationInfo info, NotificationTargetId target) {
NotificationTemplate template = setUpNotificationTemplate(notificationType, "Test", text, NotificationDeliveryMethod.WEB);
NotificationRequest request = NotificationRequest.builder()
.tenantId(tenantId)
.targets(List.of(target.getId()))
.template(template)
.info(info)
.originatorEntityId(tenantAdminUserId)
.build();
notificationCenter.processNotificationRequest(tenantId, request, null);
}
private Notification getLastNotification(User recipient) {
return await().atMost(30, TimeUnit.SECONDS)
.until(() -> notificationService.findLatestUnreadNotificationsByRecipientId(tenantId, recipient.getId(), 1).getData(),
notifications -> !notifications.isEmpty())
.get(0);
}
@Test
public void testTemplatesSearch() throws Exception {
NotificationTemplate alarmNotificationTemplate = createNotificationTemplate(NotificationType.ALARM, "Alarm", "Alarm", NotificationDeliveryMethod.WEB);

56
common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java

@ -1,56 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.util;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import static com.google.common.base.Strings.nullToEmpty;
import static org.apache.commons.lang3.StringUtils.removeStart;
public class TemplateUtils {
private static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\$\\{(.+?)(:[a-zA-Z]+)?}");
private static final Map<String, UnaryOperator<String>> FUNCTIONS = Map.of(
"upperCase", String::toUpperCase,
"lowerCase", String::toLowerCase,
"capitalize", StringUtils::capitalize
);
private TemplateUtils() {}
public static String processTemplate(String template, Map<String, String> context) {
return TEMPLATE_PARAM_PATTERN.matcher(template).replaceAll(matchResult -> {
String key = matchResult.group(1);
if (!context.containsKey(key)) {
return "\\" + matchResult.group();
}
String value = nullToEmpty(context.get(key));
String function = removeStart(matchResult.group(2), ":");
if (function != null) {
if (FUNCTIONS.containsKey(function)) {
value = FUNCTIONS.get(function).apply(value);
}
}
return value;
});
}
}

3
common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptType.java

@ -16,5 +16,6 @@
package org.thingsboard.script.api;
public enum ScriptType {
RULE_NODE_SCRIPT
RULE_NODE_SCRIPT,
TEMPLATE_SCRIPT
}

Loading…
Cancel
Save