Browse Source

add Log and Alarm nodes

pull/725/head
vparomskiy 8 years ago
parent
commit
737f2cc92a
  1. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  2. 11
      application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java
  3. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
  4. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  5. 215
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
  6. 44
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java
  7. 64
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  8. 32
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
  9. 341
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
@ -119,6 +120,11 @@ class DefaultTbContext implements TbContext {
return nodeCtx.getSelf().getId();
}
@Override
public TenantId getTenantId() {
return nodeCtx.getTenantId();
}
@Override
public void tellNext(TbMsg msg, Set<String> relationTypes) {
relationTypes.forEach(type -> tellNext(msg, type));

11
application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java

@ -145,6 +145,17 @@ public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEn
return unbindMsg(result, prevMsg);
}
@Override
public JsonNode executeJson(TbMsg msg) throws ScriptException {
return executeScript(msg);
}
@Override
public String executeToString(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
return result.asText();
}
@Override
public boolean executeFilter(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.rule.engine.api;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
@ -30,6 +31,10 @@ public interface ScriptEngine {
Set<String> executeSwitch(TbMsg msg) throws ScriptException;
JsonNode executeJson(TbMsg msg) throws ScriptException;
String executeToString(TbMsg msg) throws ScriptException;
void destroy();
}

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.api;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
@ -59,6 +60,8 @@ public interface TbContext {
RuleNodeId getSelfId();
TenantId getTenantId();
AttributesService getAttributesService();
CustomerService getCustomerService();

215
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java

@ -0,0 +1,215 @@
/**
* Copyright © 2016-2018 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.rule.engine.action;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "alarm", relationTypes = {"Created", "Updated", "Cleared", "False"},
configClazz = TbAlarmNodeConfiguration.class,
nodeDescription = "Create/Update/Clear Alarm",
nodeDetails = "isAlarm - JS function that verifies if Alarm should be CREATED for incoming message.\n" +
"isCleared - JS function that verifies if Alarm should be CLEARED for incoming message.\n" +
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
"Node output:\n" +
"If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm/isClearedAlarm' " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"})
public class TbAlarmNode implements TbNode {
static final String IS_NEW_ALARM = "isNewAlarm";
static final String IS_EXISTING_ALARM = "isExistingAlarm";
static final String IS_CLEARED_ALARM = "isClearedAlarm";
private final ObjectMapper mapper = new ObjectMapper();
private TbAlarmNodeConfiguration config;
private ScriptEngine createJsEngine;
private ScriptEngine clearJsEngine;
private ScriptEngine buildDetailsJsEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbAlarmNodeConfiguration.class);
this.createJsEngine = ctx.createJsScriptEngine(config.getCreateConditionJs(), "isAlarm");
this.clearJsEngine = ctx.createJsScriptEngine(config.getClearConditionJs(), "isCleared");
this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details");
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ListenableFuture<Boolean> shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg));
ListenableFuture<AlarmResult> transform = Futures.transform(shouldCreate, (AsyncFunction<Boolean, AlarmResult>) create -> {
if (create) {
return createOrUpdate(ctx, msg);
} else {
return checkForClearIfExist(ctx, msg);
}
});
withCallback(transform,
alarmResult -> {
if (alarmResult.alarm == null) {
ctx.tellNext(msg, "False");
} else if (alarmResult.isCreated) {
ctx.tellNext(toAlarmMsg(alarmResult, msg), "Created");
} else if (alarmResult.isUpdated) {
ctx.tellNext(toAlarmMsg(alarmResult, msg), "Updated");
} else if (alarmResult.isCleared) {
ctx.tellNext(toAlarmMsg(alarmResult, msg), "Cleared");
}
},
t -> ctx.tellError(msg, t));
}
private ListenableFuture<AlarmResult> createOrUpdate(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a == null || a.getStatus().isCleared()) {
return createNewAlarm(ctx, msg);
} else {
return updateAlarm(ctx, msg, a);
}
});
}
private ListenableFuture<AlarmResult> checkForClearIfExist(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a != null && !a.getStatus().isCleared()) {
return clearAlarm(ctx, msg, a);
}
return Futures.immediateFuture(new AlarmResult(false, false, false, null));
});
}
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm, (Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm));
return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
}
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) details -> {
alarm.setSeverity(config.getSeverity());
alarm.setPropagate(config.isPropagate());
alarm.setDetails(details);
alarm.setEndTs(System.currentTimeMillis());
return ctx.getAlarmService().createOrUpdateAlarm(alarm);
});
return Futures.transform(asyncUpdated, (Function<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
}
private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Boolean> shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg));
return Futures.transform(shouldClear, (AsyncFunction<Boolean, AlarmResult>) clear -> {
if (clear) {
ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis());
return Futures.transform(clearFuture, (Function<Boolean, AlarmResult>) cleared -> {
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
return new AlarmResult(false, false, true, alarm);
});
}
return Futures.immediateFuture(new AlarmResult(false, false, false, null));
});
}
private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
return Alarm.builder()
.tenantId(tenantId)
.originator(msg.getOriginator())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(config.getSeverity())
.propagate(config.isPropagate())
.type(config.getAlarmType())
//todo-vp: alarm date should be taken from Message or current Time should be used?
// .startTs(System.currentTimeMillis())
// .endTs(System.currentTimeMillis())
.details(details)
.build();
}
private ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg) {
return ctx.getJsExecutor().executeAsync(() -> buildDetailsJsEngine.executeJson(msg));
}
private TbMsg toAlarmMsg(AlarmResult alarmResult, TbMsg originalMsg) {
JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
String data = jsonNodes.toString();
TbMsgMetaData metaData = originalMsg.getMetaData().copy();
if (alarmResult.isCreated) {
metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isUpdated) {
metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isCleared) {
metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
}
return new TbMsg(UUIDs.timeBased(), "ALARM", originalMsg.getOriginator(), metaData, data);
}
@Override
public void destroy() {
if (createJsEngine != null) {
createJsEngine.destroy();
}
if (clearJsEngine != null) {
clearJsEngine.destroy();
}
if (buildDetailsJsEngine != null) {
buildDetailsJsEngine.destroy();
}
}
private static class AlarmResult {
boolean isCreated;
boolean isUpdated;
boolean isCleared;
Alarm alarm;
AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
this.isCreated = isCreated;
this.isUpdated = isUpdated;
this.isCleared = isCleared;
this.alarm = alarm;
}
}
}

44
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2018 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.rule.engine.action;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@Data
public class TbAlarmNodeConfiguration implements NodeConfiguration {
private String createConditionJs;
private String clearConditionJs;
private String alarmDetailsBuildJs;
private String alarmType;
private AlarmSeverity severity;
private boolean propagate;
@Override
public TbAlarmNodeConfiguration defaultConfiguration() {
TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration();
configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;");
configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;");
configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;");
configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL);
configuration.setPropagate(false);
return configuration;
}
}

64
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2018 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.rule.engine.action;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "log",
configClazz = TbLogNodeConfiguration.class,
nodeDescription = "Log incoming messages using JS script for transformation Message into String",
nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>")
public class TbLogNode implements TbNode {
private TbLogNodeConfiguration config;
private ScriptEngine jsEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "ToString");
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
toString -> {
log.info(toString);
ctx.tellNext(msg);
},
t -> ctx.tellError(msg, t));
}
@Override
public void destroy() {
if (jsEngine != null) {
jsEngine.destroy();
}
}
}

32
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2018 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.rule.engine.action;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbLogNodeConfiguration implements NodeConfiguration {
private String jsScript;
@Override
public TbLogNodeConfiguration defaultConfiguration() {
TbLogNodeConfiguration configuration = new TbLogNodeConfiguration();
configuration.setJsScript("return 'incoming message = ' + msg + meta;");
return configuration;
}
}

341
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java

@ -0,0 +1,341 @@
/**
* Copyright © 2016-2018 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.rule.engine.action;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.lang3.NotImplementedException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import javax.script.ScriptException;
import java.io.IOException;
import java.util.concurrent.Callable;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.thingsboard.rule.engine.action.TbAlarmNode.*;
import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
@RunWith(MockitoJUnitRunner.class)
public class TbAlarmNodeTest {
private TbAlarmNode node;
@Mock
private TbContext ctx;
@Mock
private ListeningExecutor executor;
@Mock
private AlarmService alarmService;
@Mock
private ScriptEngine createJs;
@Mock
private ScriptEngine clearJs;
@Mock
private ScriptEngine detailsJs;
private EntityId originator = new DeviceId(UUIDs.timeBased());
private TenantId tenantId = new TenantId(UUIDs.timeBased());
private TbMsgMetaData metaData = new TbMsgMetaData();
private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
@Test
public void newAlarmCanBeCreated() throws ScriptException, IOException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), eq("Created"));
TbMsg actualMsg = captor.getValue();
assertEquals("ALARM", actualMsg.getType());
assertEquals(originator, actualMsg.getOriginator());
assertEquals("value", actualMsg.getMetaData().getValue("key"));
assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
assertNotSame(metaData, actualMsg.getMetaData());
Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
Alarm expectedAlarm = Alarm.builder()
.tenantId(tenantId)
.originator(originator)
.status(ACTIVE_UNACK)
.severity(CRITICAL)
.propagate(true)
.type("SomeType")
.details(null)
.build();
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
}
@Test
public void shouldCreateScriptThrowsException() throws ScriptException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
when(createJs.executeFilter(msg)).thenThrow(new NotImplementedException("message"));
node.onMsg(ctx, msg);
verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
verify(ctx).createJsScriptEngine("DETAILS", "Details");
verify(ctx).getJsExecutor();
verifyNoMoreInteractions(ctx, alarmService, clearJs, detailsJs);
}
@Test
public void buildDetailsThrowsException() throws ScriptException, IOException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
node.onMsg(ctx, msg);
verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
verify(ctx).createJsScriptEngine("DETAILS", "Details");
verify(ctx, times(2)).getJsExecutor();
verify(ctx).getAlarmService();
verify(ctx).getTenantId();
verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
verifyNoMoreInteractions(ctx, alarmService, clearJs);
}
@Test
public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), eq("Created"));
TbMsg actualMsg = captor.getValue();
assertEquals("ALARM", actualMsg.getType());
assertEquals(originator, actualMsg.getOriginator());
assertEquals("value", actualMsg.getMetaData().getValue("key"));
assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
assertNotSame(metaData, actualMsg.getMetaData());
Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
Alarm expectedAlarm = Alarm.builder()
.tenantId(tenantId)
.originator(originator)
.status(ACTIVE_UNACK)
.severity(CRITICAL)
.propagate(true)
.type("SomeType")
.details(null)
.build();
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
}
@Test
public void alarmCanBeUpdated() throws ScriptException, IOException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
when(createJs.executeFilter(msg)).thenReturn(true);
when(clearJs.executeFilter(msg)).thenReturn(false);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), eq("Updated"));
TbMsg actualMsg = captor.getValue();
assertEquals("ALARM", actualMsg.getType());
assertEquals(originator, actualMsg.getOriginator());
assertEquals("value", actualMsg.getMetaData().getValue("key"));
assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_EXISTING_ALARM));
assertNotSame(metaData, actualMsg.getMetaData());
Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
assertTrue(activeAlarm.getEndTs() > oldEndDate);
Alarm expectedAlarm = Alarm.builder()
.tenantId(tenantId)
.originator(originator)
.status(ACTIVE_UNACK)
.severity(CRITICAL)
.propagate(true)
.type("SomeType")
.details(null)
.endTs(activeAlarm.getEndTs())
.build();
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
}
@Test
public void alarmCanBeCleared() throws ScriptException, IOException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
when(createJs.executeFilter(msg)).thenReturn(false);
when(clearJs.executeFilter(msg)).thenReturn(true);
// when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
when(alarmService.clearAlarm(eq(activeAlarm.getId()), anyLong())).thenReturn(Futures.immediateFuture(true));
// doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), eq("Cleared"));
TbMsg actualMsg = captor.getValue();
assertEquals("ALARM", actualMsg.getType());
assertEquals(originator, actualMsg.getOriginator());
assertEquals("value", actualMsg.getMetaData().getValue("key"));
assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_CLEARED_ALARM));
assertNotSame(metaData, actualMsg.getMetaData());
Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
Alarm expectedAlarm = Alarm.builder()
.tenantId(tenantId)
.originator(originator)
.status(CLEARED_UNACK)
.severity(WARNING)
.propagate(false)
.type("SomeType")
.details(null)
.endTs(oldEndDate)
.build();
assertEquals(expectedAlarm, actualAlarm);
}
private void initWithScript() {
try {
TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration();
config.setPropagate(true);
config.setSeverity(CRITICAL);
config.setAlarmType("SomeType");
config.setCreateConditionJs("CREATE");
config.setClearConditionJs("CLEAR");
config.setAlarmDetailsBuildJs("DETAILS");
ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("CREATE", "isAlarm")).thenReturn(createJs);
when(ctx.createJsScriptEngine("CLEAR", "isCleared")).thenReturn(clearJs);
when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId);
when(ctx.getJsExecutor()).thenReturn(executor);
when(ctx.getAlarmService()).thenReturn(alarmService);
mockJsExecutor();
node = new TbAlarmNode();
node.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) {
throw new IllegalStateException(ex);
}
}
private void mockJsExecutor() {
when(ctx.getJsExecutor()).thenReturn(executor);
doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
try {
Callable task = (Callable) (invocationOnMock.getArguments())[0];
return Futures.immediateFuture((Boolean) task.call());
} catch (Throwable th) {
return Futures.immediateFailedFuture(th);
}
}).when(executor).executeAsync(any(Callable.class));
}
private void verifyError(TbMsg msg, String message, Class expectedClass) {
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellError(same(msg), captor.capture());
Throwable value = captor.getValue();
assertEquals(expectedClass, value.getClass());
assertEquals(message, value.getMessage());
}
}
Loading…
Cancel
Save