340 changed files with 16506 additions and 2390 deletions
@ -1,117 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import akka.actor.ActorRef; |
|
||||
import org.thingsboard.server.common.msg.core.RuleEngineError; |
|
||||
import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg; |
|
||||
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; |
|
||||
import org.thingsboard.server.common.msg.session.ToDeviceMsg; |
|
||||
import org.thingsboard.server.extensions.api.device.DeviceAttributes; |
|
||||
import org.thingsboard.server.extensions.api.device.DeviceMetaData; |
|
||||
|
|
||||
public class ChainProcessingContext { |
|
||||
|
|
||||
private final ChainProcessingMetaData md; |
|
||||
private final int index; |
|
||||
private final RuleEngineError error; |
|
||||
private ToDeviceMsg response; |
|
||||
|
|
||||
|
|
||||
public ChainProcessingContext(ChainProcessingMetaData md) { |
|
||||
super(); |
|
||||
this.md = md; |
|
||||
this.index = 0; |
|
||||
this.error = RuleEngineError.NO_RULES; |
|
||||
} |
|
||||
|
|
||||
private ChainProcessingContext(ChainProcessingContext other, int indexOffset, RuleEngineError error) { |
|
||||
super(); |
|
||||
this.md = other.md; |
|
||||
this.index = other.index + indexOffset; |
|
||||
this.error = error; |
|
||||
this.response = other.response; |
|
||||
|
|
||||
if (this.index < 0 || this.index >= this.md.chain.size()) { |
|
||||
throw new IllegalArgumentException("Can't apply offset " + indexOffset + " to the chain!"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public ActorRef getDeviceActor() { |
|
||||
return md.originator; |
|
||||
} |
|
||||
|
|
||||
public ActorRef getCurrentActor() { |
|
||||
return md.chain.getRuleActorMd(index).getActorRef(); |
|
||||
} |
|
||||
|
|
||||
public boolean hasNext() { |
|
||||
return (getChainLength() - 1) > index; |
|
||||
} |
|
||||
|
|
||||
public boolean isFailure() { |
|
||||
return (error != null && error.isCritical()) || (response != null && !response.isSuccess()); |
|
||||
} |
|
||||
|
|
||||
public ChainProcessingContext getNext() { |
|
||||
return new ChainProcessingContext(this, 1, this.error); |
|
||||
} |
|
||||
|
|
||||
public ChainProcessingContext withError(RuleEngineError error) { |
|
||||
if (error != null && (this.error == null || this.error.getPriority() < error.getPriority())) { |
|
||||
return new ChainProcessingContext(this, 0, error); |
|
||||
} else { |
|
||||
return this; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public int getChainLength() { |
|
||||
return md.chain.size(); |
|
||||
} |
|
||||
|
|
||||
public ToDeviceActorMsg getInMsg() { |
|
||||
return md.inMsg; |
|
||||
} |
|
||||
|
|
||||
public DeviceMetaData getDeviceMetaData() { |
|
||||
return md.deviceMetaData; |
|
||||
} |
|
||||
|
|
||||
public String getDeviceName() { |
|
||||
return md.deviceMetaData.getDeviceName(); |
|
||||
} |
|
||||
|
|
||||
public String getDeviceType() { |
|
||||
return md.deviceMetaData.getDeviceType(); |
|
||||
} |
|
||||
|
|
||||
public DeviceAttributes getAttributes() { |
|
||||
return md.deviceMetaData.getDeviceAttributes(); |
|
||||
} |
|
||||
|
|
||||
public ToDeviceMsg getResponse() { |
|
||||
return response; |
|
||||
} |
|
||||
|
|
||||
public void mergeResponse(ToDeviceMsg response) { |
|
||||
// TODO add merge logic
|
|
||||
this.response = response; |
|
||||
} |
|
||||
|
|
||||
public RuleEngineErrorMsg getError() { |
|
||||
return new RuleEngineErrorMsg(md.inMsg.getPayload().getMsgType(), error); |
|
||||
} |
|
||||
} |
|
||||
@ -1,41 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import akka.actor.ActorRef; |
|
||||
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; |
|
||||
import org.thingsboard.server.extensions.api.device.DeviceMetaData; |
|
||||
|
|
||||
/** |
|
||||
* Immutable part of chain processing data; |
|
||||
* |
|
||||
* @author ashvayka |
|
||||
*/ |
|
||||
public final class ChainProcessingMetaData { |
|
||||
|
|
||||
final RuleActorChain chain; |
|
||||
final ToDeviceActorMsg inMsg; |
|
||||
final ActorRef originator; |
|
||||
final DeviceMetaData deviceMetaData; |
|
||||
|
|
||||
public ChainProcessingMetaData(RuleActorChain chain, ToDeviceActorMsg inMsg, DeviceMetaData deviceMetaData, ActorRef originator) { |
|
||||
super(); |
|
||||
this.chain = chain; |
|
||||
this.inMsg = inMsg; |
|
||||
this.originator = originator; |
|
||||
this.deviceMetaData = deviceMetaData; |
|
||||
} |
|
||||
} |
|
||||
@ -1,43 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
public class ComplexRuleActorChain implements RuleActorChain { |
|
||||
|
|
||||
private final RuleActorChain systemChain; |
|
||||
private final RuleActorChain tenantChain; |
|
||||
|
|
||||
public ComplexRuleActorChain(RuleActorChain systemChain, RuleActorChain tenantChain) { |
|
||||
super(); |
|
||||
this.systemChain = systemChain; |
|
||||
this.tenantChain = tenantChain; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public int size() { |
|
||||
return systemChain.size() + tenantChain.size(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public RuleActorMetaData getRuleActorMd(int index) { |
|
||||
if (index < systemChain.size()) { |
|
||||
return systemChain.getRuleActorMd(index); |
|
||||
} else { |
|
||||
return tenantChain.getRuleActorMd(index - systemChain.size()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,90 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import org.thingsboard.server.actors.ActorSystemContext; |
|
||||
import org.thingsboard.server.actors.service.ComponentActor; |
|
||||
import org.thingsboard.server.actors.service.ContextBasedCreator; |
|
||||
import org.thingsboard.server.actors.stats.StatsPersistTick; |
|
||||
import org.thingsboard.server.common.data.id.RuleId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; |
|
||||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|
||||
import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg; |
|
||||
|
|
||||
public class RuleActor extends ComponentActor<RuleId, RuleActorMessageProcessor> { |
|
||||
|
|
||||
private RuleActor(ActorSystemContext systemContext, TenantId tenantId, RuleId ruleId) { |
|
||||
super(systemContext, tenantId, ruleId); |
|
||||
setProcessor(new RuleActorMessageProcessor(tenantId, ruleId, systemContext, logger)); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onReceive(Object msg) throws Exception { |
|
||||
logger.debug("[{}] Received message: {}", id, msg); |
|
||||
if (msg instanceof RuleProcessingMsg) { |
|
||||
try { |
|
||||
processor.onRuleProcessingMsg(context(), (RuleProcessingMsg) msg); |
|
||||
increaseMessagesProcessedCount(); |
|
||||
} catch (Exception e) { |
|
||||
logAndPersist("onDeviceMsg", e); |
|
||||
} |
|
||||
} else if (msg instanceof PluginToRuleMsg<?>) { |
|
||||
try { |
|
||||
processor.onPluginMsg(context(), (PluginToRuleMsg<?>) msg); |
|
||||
} catch (Exception e) { |
|
||||
logAndPersist("onPluginMsg", e); |
|
||||
} |
|
||||
} else if (msg instanceof ComponentLifecycleMsg) { |
|
||||
onComponentLifecycleMsg((ComponentLifecycleMsg) msg); |
|
||||
} else if (msg instanceof ClusterEventMsg) { |
|
||||
onClusterEventMsg((ClusterEventMsg) msg); |
|
||||
} else if (msg instanceof RuleToPluginTimeoutMsg) { |
|
||||
try { |
|
||||
processor.onTimeoutMsg(context(), (RuleToPluginTimeoutMsg) msg); |
|
||||
} catch (Exception e) { |
|
||||
logAndPersist("onTimeoutMsg", e); |
|
||||
} |
|
||||
} else if (msg instanceof StatsPersistTick) { |
|
||||
onStatsPersistTick(id); |
|
||||
} else { |
|
||||
logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static class ActorCreator extends ContextBasedCreator<RuleActor> { |
|
||||
private static final long serialVersionUID = 1L; |
|
||||
|
|
||||
private final TenantId tenantId; |
|
||||
private final RuleId ruleId; |
|
||||
|
|
||||
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleId ruleId) { |
|
||||
super(context); |
|
||||
this.tenantId = tenantId; |
|
||||
this.ruleId = ruleId; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public RuleActor create() throws Exception { |
|
||||
return new RuleActor(context, tenantId, ruleId); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected long getErrorPersistFrequency() { |
|
||||
return systemContext.getRuleErrorPersistFrequency(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,345 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import java.util.*; |
|
||||
|
|
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
|
||||
import org.springframework.util.StringUtils; |
|
||||
import org.thingsboard.server.actors.ActorSystemContext; |
|
||||
import org.thingsboard.server.actors.plugin.RuleToPluginMsgWrapper; |
|
||||
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; |
|
||||
import org.thingsboard.server.common.data.id.PluginId; |
|
||||
import org.thingsboard.server.common.data.id.RuleId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; |
|
||||
import org.thingsboard.server.common.data.plugin.PluginMetaData; |
|
||||
import org.thingsboard.server.common.data.rule.RuleMetaData; |
|
||||
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; |
|
||||
import org.thingsboard.server.common.msg.core.BasicRequest; |
|
||||
import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse; |
|
||||
import org.thingsboard.server.common.msg.core.RuleEngineError; |
|
||||
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; |
|
||||
import org.thingsboard.server.common.msg.session.MsgType; |
|
||||
import org.thingsboard.server.common.msg.session.ToDeviceMsg; |
|
||||
import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException; |
|
||||
import org.thingsboard.server.extensions.api.rules.*; |
|
||||
import org.thingsboard.server.extensions.api.plugins.PluginAction; |
|
||||
import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg; |
|
||||
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg; |
|
||||
|
|
||||
import com.fasterxml.jackson.databind.JsonNode; |
|
||||
|
|
||||
import akka.actor.ActorContext; |
|
||||
import akka.actor.ActorRef; |
|
||||
import akka.event.LoggingAdapter; |
|
||||
|
|
||||
class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> { |
|
||||
|
|
||||
private final RuleProcessingContext ruleCtx; |
|
||||
private final Map<UUID, RuleProcessingMsg> pendingMsgMap; |
|
||||
|
|
||||
private RuleMetaData ruleMd; |
|
||||
private ComponentLifecycleState state; |
|
||||
private List<RuleFilter> filters; |
|
||||
private RuleProcessor processor; |
|
||||
private PluginAction action; |
|
||||
|
|
||||
private TenantId pluginTenantId; |
|
||||
private PluginId pluginId; |
|
||||
|
|
||||
protected RuleActorMessageProcessor(TenantId tenantId, RuleId ruleId, ActorSystemContext systemContext, LoggingAdapter logger) { |
|
||||
super(systemContext, logger, tenantId, ruleId); |
|
||||
this.pendingMsgMap = new HashMap<>(); |
|
||||
this.ruleCtx = new RuleProcessingContext(systemContext, ruleId); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void start() throws Exception { |
|
||||
logger.info("[{}][{}] Starting rule actor.", entityId, tenantId); |
|
||||
ruleMd = systemContext.getRuleService().findRuleById(entityId); |
|
||||
if (ruleMd == null) { |
|
||||
throw new RuleInitializationException("Rule not found!"); |
|
||||
} |
|
||||
state = ruleMd.getState(); |
|
||||
if (state == ComponentLifecycleState.ACTIVE) { |
|
||||
logger.info("[{}] Rule is active. Going to initialize rule components.", entityId); |
|
||||
initComponent(); |
|
||||
} else { |
|
||||
logger.info("[{}] Rule is suspended. Skipping rule components initialization.", entityId); |
|
||||
} |
|
||||
|
|
||||
logger.info("[{}][{}] Started rule actor.", entityId, tenantId); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void stop() throws Exception { |
|
||||
onStop(); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private void initComponent() throws RuleException { |
|
||||
try { |
|
||||
if (!ruleMd.getFilters().isArray()) { |
|
||||
throw new RuntimeException("Filters are not array!"); |
|
||||
} |
|
||||
fetchPluginInfo(); |
|
||||
initFilters(); |
|
||||
initProcessor(); |
|
||||
initAction(); |
|
||||
} catch (RuntimeException e) { |
|
||||
throw new RuleInitializationException("Unknown runtime exception!", e); |
|
||||
} catch (InstantiationException e) { |
|
||||
throw new RuleInitializationException("No default constructor for rule implementation!", e); |
|
||||
} catch (IllegalAccessException e) { |
|
||||
throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e); |
|
||||
} catch (ClassNotFoundException e) { |
|
||||
throw new RuleInitializationException("Rule Class not found!", e); |
|
||||
} catch (Exception e) { |
|
||||
throw new RuleException(e.getMessage(), e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void initAction() throws Exception { |
|
||||
if (ruleMd.getAction() != null && !ruleMd.getAction().isNull()) { |
|
||||
action = initComponent(ruleMd.getAction()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void initProcessor() throws Exception { |
|
||||
if (ruleMd.getProcessor() != null && !ruleMd.getProcessor().isNull()) { |
|
||||
processor = initComponent(ruleMd.getProcessor()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void initFilters() throws Exception { |
|
||||
filters = new ArrayList<>(ruleMd.getFilters().size()); |
|
||||
for (int i = 0; i < ruleMd.getFilters().size(); i++) { |
|
||||
filters.add(initComponent(ruleMd.getFilters().get(i))); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void fetchPluginInfo() { |
|
||||
if (!StringUtils.isEmpty(ruleMd.getPluginToken())) { |
|
||||
PluginMetaData pluginMd = systemContext.getPluginService().findPluginByApiToken(ruleMd.getPluginToken()); |
|
||||
pluginTenantId = pluginMd.getTenantId(); |
|
||||
pluginId = pluginMd.getId(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected void onRuleProcessingMsg(ActorContext context, RuleProcessingMsg msg) throws RuleException { |
|
||||
if (state != ComponentLifecycleState.ACTIVE) { |
|
||||
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_ACTIVE_RULES); |
|
||||
return; |
|
||||
} |
|
||||
ChainProcessingContext chainCtx = msg.getCtx(); |
|
||||
ToDeviceActorMsg inMsg = chainCtx.getInMsg(); |
|
||||
|
|
||||
ruleCtx.update(inMsg, chainCtx.getDeviceMetaData()); |
|
||||
|
|
||||
logger.debug("[{}] Going to filter in msg: {}", entityId, inMsg); |
|
||||
for (RuleFilter filter : filters) { |
|
||||
if (!filter.filter(ruleCtx, inMsg)) { |
|
||||
logger.debug("[{}] In msg is NOT valid for processing by current rule: {}", entityId, inMsg); |
|
||||
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_FILTERS_MATCHED); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
RuleProcessingMetaData inMsgMd; |
|
||||
if (processor != null) { |
|
||||
logger.debug("[{}] Going to process in msg: {}", entityId, inMsg); |
|
||||
inMsgMd = processor.process(ruleCtx, inMsg); |
|
||||
} else { |
|
||||
inMsgMd = new RuleProcessingMetaData(); |
|
||||
} |
|
||||
logger.debug("[{}] Going to convert in msg: {}", entityId, inMsg); |
|
||||
if (action != null) { |
|
||||
Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd); |
|
||||
if (ruleToPluginMsgOptional.isPresent()) { |
|
||||
RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get(); |
|
||||
logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg); |
|
||||
context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self()); |
|
||||
if (action.isOneWayAction()) { |
|
||||
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); |
|
||||
return; |
|
||||
} else { |
|
||||
pendingMsgMap.put(ruleToPluginMsg.getUid(), msg); |
|
||||
scheduleMsgWithDelay(context, new RuleToPluginTimeoutMsg(ruleToPluginMsg.getUid()), systemContext.getPluginProcessingTimeout()); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
logger.debug("[{}] Nothing to send to plugin: {}", entityId, pluginId); |
|
||||
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); |
|
||||
} |
|
||||
|
|
||||
void onPluginMsg(ActorContext context, PluginToRuleMsg<?> msg) { |
|
||||
RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getUid()); |
|
||||
if (pendingMsg != null) { |
|
||||
ChainProcessingContext ctx = pendingMsg.getCtx(); |
|
||||
Optional<ToDeviceMsg> ruleResponseOptional = action.convert(msg); |
|
||||
if (ruleResponseOptional.isPresent()) { |
|
||||
ctx.mergeResponse(ruleResponseOptional.get()); |
|
||||
pushToNextRule(context, ctx, null); |
|
||||
} else { |
|
||||
pushToNextRule(context, ctx, RuleEngineError.NO_RESPONSE_FROM_ACTIONS); |
|
||||
} |
|
||||
} else { |
|
||||
logger.warning("[{}] Processing timeout detected: [{}]", entityId, msg.getUid()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void onTimeoutMsg(ActorContext context, RuleToPluginTimeoutMsg msg) { |
|
||||
RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getMsgId()); |
|
||||
if (pendingMsg != null) { |
|
||||
logger.debug("[{}] Processing timeout detected [{}]: {}", entityId, msg.getMsgId(), pendingMsg); |
|
||||
ChainProcessingContext ctx = pendingMsg.getCtx(); |
|
||||
pushToNextRule(context, ctx, RuleEngineError.PLUGIN_TIMEOUT); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void pushToNextRule(ActorContext context, ChainProcessingContext ctx, RuleEngineError error) { |
|
||||
if (error != null) { |
|
||||
ctx = ctx.withError(error); |
|
||||
} |
|
||||
if (ctx.isFailure()) { |
|
||||
logger.debug("[{}][{}] Forwarding processing chain to device actor due to failure.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); |
|
||||
ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender()); |
|
||||
} else if (!ctx.hasNext()) { |
|
||||
logger.debug("[{}][{}] Forwarding processing chain to device actor due to end of chain.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); |
|
||||
ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender()); |
|
||||
} else { |
|
||||
logger.debug("[{}][{}] Forwarding processing chain to next rule actor.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); |
|
||||
ChainProcessingContext nextTask = ctx.getNext(); |
|
||||
nextTask.getCurrentActor().tell(new RuleProcessingMsg(nextTask), context.self()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onCreated(ActorContext context) { |
|
||||
logger.info("[{}] Going to process onCreated rule.", entityId); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onUpdate(ActorContext context) throws RuleException { |
|
||||
RuleMetaData oldRuleMd = ruleMd; |
|
||||
ruleMd = systemContext.getRuleService().findRuleById(entityId); |
|
||||
logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd); |
|
||||
try { |
|
||||
fetchPluginInfo(); |
|
||||
if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) { |
|
||||
logger.info("[{}] Rule filters require restart due to json change from {} to {}.", |
|
||||
entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters())); |
|
||||
stopFilters(); |
|
||||
initFilters(); |
|
||||
} |
|
||||
if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) { |
|
||||
logger.info("[{}] Rule processor require restart due to configuration change.", entityId); |
|
||||
stopProcessor(); |
|
||||
initProcessor(); |
|
||||
} |
|
||||
if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) { |
|
||||
logger.info("[{}] Rule action require restart due to configuration change.", entityId); |
|
||||
stopAction(); |
|
||||
initAction(); |
|
||||
} |
|
||||
} catch (RuntimeException e) { |
|
||||
throw new RuleInitializationException("Unknown runtime exception!", e); |
|
||||
} catch (InstantiationException e) { |
|
||||
throw new RuleInitializationException("No default constructor for rule implementation!", e); |
|
||||
} catch (IllegalAccessException e) { |
|
||||
throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e); |
|
||||
} catch (ClassNotFoundException e) { |
|
||||
throw new RuleInitializationException("Rule Class not found!", e); |
|
||||
} catch (JsonProcessingException e) { |
|
||||
throw new RuleInitializationException("Rule configuration is invalid!", e); |
|
||||
} catch (Exception e) { |
|
||||
throw new RuleInitializationException(e.getMessage(), e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onActivate(ActorContext context) throws Exception { |
|
||||
logger.info("[{}] Going to process onActivate rule.", entityId); |
|
||||
this.state = ComponentLifecycleState.ACTIVE; |
|
||||
if (filters != null) { |
|
||||
filters.forEach(RuleLifecycleComponent::resume); |
|
||||
if (processor != null) { |
|
||||
processor.resume(); |
|
||||
} else { |
|
||||
initProcessor(); |
|
||||
} |
|
||||
if (action != null) { |
|
||||
action.resume(); |
|
||||
} |
|
||||
logger.info("[{}] Rule resumed.", entityId); |
|
||||
} else { |
|
||||
start(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onSuspend(ActorContext context) { |
|
||||
logger.info("[{}] Going to process onSuspend rule.", entityId); |
|
||||
this.state = ComponentLifecycleState.SUSPENDED; |
|
||||
if (filters != null) { |
|
||||
filters.forEach(f -> f.suspend()); |
|
||||
} |
|
||||
if (processor != null) { |
|
||||
processor.suspend(); |
|
||||
} |
|
||||
if (action != null) { |
|
||||
action.suspend(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onStop(ActorContext context) { |
|
||||
logger.info("[{}] Going to process onStop rule.", entityId); |
|
||||
onStop(); |
|
||||
scheduleMsgWithDelay(context, new RuleTerminationMsg(entityId), systemContext.getRuleActorTerminationDelay()); |
|
||||
} |
|
||||
|
|
||||
private void onStop() { |
|
||||
this.state = ComponentLifecycleState.SUSPENDED; |
|
||||
stopFilters(); |
|
||||
stopProcessor(); |
|
||||
stopAction(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onClusterEventMsg(ClusterEventMsg msg) throws Exception { |
|
||||
//Do nothing
|
|
||||
} |
|
||||
|
|
||||
private void stopAction() { |
|
||||
if (action != null) { |
|
||||
action.stop(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void stopProcessor() { |
|
||||
if (processor != null) { |
|
||||
processor.stop(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void stopFilters() { |
|
||||
if (filters != null) { |
|
||||
filters.forEach(f -> f.stop()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,107 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import java.util.Comparator; |
|
||||
|
|
||||
import org.thingsboard.server.common.data.id.RuleId; |
|
||||
|
|
||||
import akka.actor.ActorRef; |
|
||||
|
|
||||
public class RuleActorMetaData { |
|
||||
|
|
||||
private final RuleId ruleId; |
|
||||
private final boolean systemRule; |
|
||||
private final int weight; |
|
||||
private final ActorRef actorRef; |
|
||||
|
|
||||
public static final Comparator<RuleActorMetaData> RULE_ACTOR_MD_COMPARATOR = new Comparator<RuleActorMetaData>() { |
|
||||
|
|
||||
@Override |
|
||||
public int compare(RuleActorMetaData r1, RuleActorMetaData r2) { |
|
||||
if (r1.isSystemRule() && !r2.isSystemRule()) { |
|
||||
return 1; |
|
||||
} else if (!r1.isSystemRule() && r2.isSystemRule()) { |
|
||||
return -1; |
|
||||
} else { |
|
||||
return Integer.compare(r2.getWeight(), r1.getWeight()); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
public static RuleActorMetaData systemRule(RuleId ruleId, int weight, ActorRef actorRef) { |
|
||||
return new RuleActorMetaData(ruleId, true, weight, actorRef); |
|
||||
} |
|
||||
|
|
||||
public static RuleActorMetaData tenantRule(RuleId ruleId, int weight, ActorRef actorRef) { |
|
||||
return new RuleActorMetaData(ruleId, false, weight, actorRef); |
|
||||
} |
|
||||
|
|
||||
private RuleActorMetaData(RuleId ruleId, boolean systemRule, int weight, ActorRef actorRef) { |
|
||||
super(); |
|
||||
this.ruleId = ruleId; |
|
||||
this.systemRule = systemRule; |
|
||||
this.weight = weight; |
|
||||
this.actorRef = actorRef; |
|
||||
} |
|
||||
|
|
||||
public RuleId getRuleId() { |
|
||||
return ruleId; |
|
||||
} |
|
||||
|
|
||||
public boolean isSystemRule() { |
|
||||
return systemRule; |
|
||||
} |
|
||||
|
|
||||
public int getWeight() { |
|
||||
return weight; |
|
||||
} |
|
||||
|
|
||||
public ActorRef getActorRef() { |
|
||||
return actorRef; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public int hashCode() { |
|
||||
final int prime = 31; |
|
||||
int result = 1; |
|
||||
result = prime * result + ((ruleId == null) ? 0 : ruleId.hashCode()); |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public boolean equals(Object obj) { |
|
||||
if (this == obj) |
|
||||
return true; |
|
||||
if (obj == null) |
|
||||
return false; |
|
||||
if (getClass() != obj.getClass()) |
|
||||
return false; |
|
||||
RuleActorMetaData other = (RuleActorMetaData) obj; |
|
||||
if (ruleId == null) { |
|
||||
if (other.ruleId != null) |
|
||||
return false; |
|
||||
} else if (!ruleId.equals(other.ruleId)) |
|
||||
return false; |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public String toString() { |
|
||||
return "RuleActorMetaData [ruleId=" + ruleId + ", systemRule=" + systemRule + ", weight=" + weight + ", actorRef=" + actorRef + "]"; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,115 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.rule; |
|
||||
|
|
||||
import com.google.common.util.concurrent.ListenableFuture; |
|
||||
import org.thingsboard.server.actors.ActorSystemContext; |
|
||||
import org.thingsboard.server.common.data.Event; |
|
||||
import org.thingsboard.server.common.data.alarm.Alarm; |
|
||||
import org.thingsboard.server.common.data.alarm.AlarmId; |
|
||||
import org.thingsboard.server.common.data.id.*; |
|
||||
import org.thingsboard.server.dao.alarm.AlarmService; |
|
||||
import org.thingsboard.server.dao.event.EventService; |
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|
||||
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; |
|
||||
import org.thingsboard.server.extensions.api.device.DeviceMetaData; |
|
||||
import org.thingsboard.server.extensions.api.rules.RuleContext; |
|
||||
|
|
||||
import java.util.Optional; |
|
||||
import java.util.concurrent.ExecutionException; |
|
||||
|
|
||||
public class RuleProcessingContext implements RuleContext { |
|
||||
|
|
||||
private final TimeseriesService tsService; |
|
||||
private final EventService eventService; |
|
||||
private final AlarmService alarmService; |
|
||||
private final RuleId ruleId; |
|
||||
private TenantId tenantId; |
|
||||
private CustomerId customerId; |
|
||||
private DeviceId deviceId; |
|
||||
private DeviceMetaData deviceMetaData; |
|
||||
|
|
||||
RuleProcessingContext(ActorSystemContext systemContext, RuleId ruleId) { |
|
||||
this.tsService = systemContext.getTsService(); |
|
||||
this.eventService = systemContext.getEventService(); |
|
||||
this.alarmService = systemContext.getAlarmService(); |
|
||||
this.ruleId = ruleId; |
|
||||
} |
|
||||
|
|
||||
void update(ToDeviceActorMsg toDeviceActorMsg, DeviceMetaData deviceMetaData) { |
|
||||
this.tenantId = toDeviceActorMsg.getTenantId(); |
|
||||
this.customerId = toDeviceActorMsg.getCustomerId(); |
|
||||
this.deviceId = toDeviceActorMsg.getDeviceId(); |
|
||||
this.deviceMetaData = deviceMetaData; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public RuleId getRuleId() { |
|
||||
return ruleId; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public DeviceMetaData getDeviceMetaData() { |
|
||||
return deviceMetaData; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public Event save(Event event) { |
|
||||
checkEvent(event); |
|
||||
return eventService.save(event); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public Optional<Event> saveIfNotExists(Event event) { |
|
||||
checkEvent(event); |
|
||||
return eventService.saveIfNotExists(event); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public Optional<Event> findEvent(String eventType, String eventUid) { |
|
||||
return eventService.findEvent(tenantId, deviceId, eventType, eventUid); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public Alarm createOrUpdateAlarm(Alarm alarm) { |
|
||||
alarm.setTenantId(tenantId); |
|
||||
return alarmService.createOrUpdateAlarm(alarm); |
|
||||
} |
|
||||
|
|
||||
public Optional<Alarm> findLatestAlarm(EntityId originator, String alarmType) { |
|
||||
try { |
|
||||
return Optional.ofNullable(alarmService.findLatestByOriginatorAndType(tenantId, originator, alarmType).get()); |
|
||||
} catch (InterruptedException | ExecutionException e) { |
|
||||
throw new RuntimeException("Failed to lookup alarm!", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long clearTs) { |
|
||||
return alarmService.clearAlarm(alarmId, clearTs); |
|
||||
} |
|
||||
|
|
||||
private void checkEvent(Event event) { |
|
||||
if (event.getTenantId() == null) { |
|
||||
event.setTenantId(tenantId); |
|
||||
} else if (!tenantId.equals(event.getTenantId())) { |
|
||||
throw new IllegalArgumentException("Invalid Tenant id!"); |
|
||||
} |
|
||||
if (event.getEntityId() == null) { |
|
||||
event.setEntityId(deviceId); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,209 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.ActorRef; |
||||
|
import akka.actor.Cancellable; |
||||
|
import com.google.common.base.Function; |
||||
|
import org.thingsboard.rule.engine.api.*; |
||||
|
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; |
||||
|
import org.thingsboard.server.dao.alarm.AlarmService; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.plugin.PluginService; |
||||
|
import org.thingsboard.server.dao.relation.RelationService; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
import org.thingsboard.server.service.script.NashornJsEngine; |
||||
|
import scala.concurrent.duration.Duration; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.03.18. |
||||
|
*/ |
||||
|
class DefaultTbContext implements TbContext { |
||||
|
|
||||
|
private static final Function<? super List<Void>, ? extends Void> LIST_VOID_FUNCTION = v -> null; |
||||
|
private final ActorSystemContext mainCtx; |
||||
|
private final RuleNodeCtx nodeCtx; |
||||
|
|
||||
|
public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) { |
||||
|
this.mainCtx = mainCtx; |
||||
|
this.nodeCtx = nodeCtx; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellNext(TbMsg msg) { |
||||
|
tellNext(msg, (String) null); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellNext(TbMsg msg, String relationType) { |
||||
|
if (nodeCtx.getSelf().isDebugMode()) { |
||||
|
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg); |
||||
|
} |
||||
|
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationType, msg), nodeCtx.getSelfActor()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellSelf(TbMsg msg, long delayMs) { |
||||
|
//TODO: add persistence layer
|
||||
|
scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor()); |
||||
|
} |
||||
|
|
||||
|
private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) { |
||||
|
mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellOthers(TbMsg msg) { |
||||
|
throw new RuntimeException("Not Implemented!"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellSibling(TbMsg msg, ServerAddress address) { |
||||
|
throw new RuntimeException("Not Implemented!"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void spawn(TbMsg msg) { |
||||
|
throw new RuntimeException("Not Implemented!"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void ack(TbMsg msg) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void tellError(TbMsg msg, Throwable th) { |
||||
|
if (nodeCtx.getSelf().isDebugMode()) { |
||||
|
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, th); |
||||
|
} |
||||
|
nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void updateSelf(RuleNode self) { |
||||
|
nodeCtx.setSelf(self); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleNodeId getSelfId() { |
||||
|
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)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListeningExecutor getJsExecutor() { |
||||
|
return mainCtx.getJsExecutor(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListeningExecutor getMailExecutor() { |
||||
|
return mainCtx.getMailExecutor(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListeningExecutor getDbCallbackExecutor() { |
||||
|
return mainCtx.getDbCallbackExecutor(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) { |
||||
|
return new NashornJsEngine(script, functionName, argNames); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AttributesService getAttributesService() { |
||||
|
return mainCtx.getAttributesService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public CustomerService getCustomerService() { |
||||
|
return mainCtx.getCustomerService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public UserService getUserService() { |
||||
|
return mainCtx.getUserService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public PluginService getPluginService() { |
||||
|
return mainCtx.getPluginService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AssetService getAssetService() { |
||||
|
return mainCtx.getAssetService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public DeviceService getDeviceService() { |
||||
|
return mainCtx.getDeviceService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AlarmService getAlarmService() { |
||||
|
return mainCtx.getAlarmService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleChainService getRuleChainService() { |
||||
|
return mainCtx.getRuleChainService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TimeseriesService getTimeseriesService() { |
||||
|
return mainCtx.getTsService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleEngineTelemetryService getTelemetryService() { |
||||
|
return mainCtx.getTsSubService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RelationService getRelationService() { |
||||
|
return mainCtx.getRelationService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public MailService getMailService() { |
||||
|
return mainCtx.getMailService(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.OneForOneStrategy; |
||||
|
import akka.actor.SupervisorStrategy; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.service.ComponentActor; |
||||
|
import org.thingsboard.server.actors.service.ContextBasedCreator; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.msg.TbActorMsg; |
||||
|
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
||||
|
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; |
||||
|
import scala.concurrent.duration.Duration; |
||||
|
|
||||
|
public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> { |
||||
|
|
||||
|
private RuleChainActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId) { |
||||
|
super(systemContext, tenantId, ruleChainId); |
||||
|
setProcessor(new RuleChainActorMessageProcessor(tenantId, ruleChainId, systemContext, |
||||
|
logger, context().parent(), context().self())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean process(TbActorMsg msg) { |
||||
|
switch (msg.getMsgType()) { |
||||
|
case COMPONENT_LIFE_CYCLE_MSG: |
||||
|
onComponentLifecycleMsg((ComponentLifecycleMsg) msg); |
||||
|
break; |
||||
|
case SERVICE_TO_RULE_ENGINE_MSG: |
||||
|
processor.onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg); |
||||
|
break; |
||||
|
case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG: |
||||
|
processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg); |
||||
|
break; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public static class ActorCreator extends ContextBasedCreator<RuleChainActor> { |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final RuleChainId ruleChainId; |
||||
|
|
||||
|
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId pluginId) { |
||||
|
super(context); |
||||
|
this.tenantId = tenantId; |
||||
|
this.ruleChainId = pluginId; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleChainActor create() throws Exception { |
||||
|
return new RuleChainActor(context, tenantId, ruleChainId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected long getErrorPersistFrequency() { |
||||
|
return systemContext.getRuleChainErrorPersistFrequency(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public SupervisorStrategy supervisorStrategy() { |
||||
|
return strategy; |
||||
|
} |
||||
|
|
||||
|
private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), t -> { |
||||
|
logAndPersist("Unknown Failure", ActorSystemContext.toException(t)); |
||||
|
return SupervisorStrategy.resume(); |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.ActorContext; |
||||
|
import akka.actor.ActorRef; |
||||
|
import akka.actor.Props; |
||||
|
import akka.event.LoggingAdapter; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.service.DefaultActorService; |
||||
|
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.rule.RuleChain; |
||||
|
import org.thingsboard.server.common.data.rule.RuleNode; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; |
||||
|
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
||||
|
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* @author Andrew Shvayka |
||||
|
*/ |
||||
|
public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleChainId> { |
||||
|
|
||||
|
private final ActorRef parent; |
||||
|
private final ActorRef self; |
||||
|
private final Map<RuleNodeId, RuleNodeCtx> nodeActors; |
||||
|
private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes; |
||||
|
private final RuleChainService service; |
||||
|
|
||||
|
private RuleNodeId firstId; |
||||
|
private RuleNodeCtx firstNode; |
||||
|
|
||||
|
RuleChainActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, ActorSystemContext systemContext |
||||
|
, LoggingAdapter logger, ActorRef parent, ActorRef self) { |
||||
|
super(systemContext, logger, tenantId, ruleChainId); |
||||
|
this.parent = parent; |
||||
|
this.self = self; |
||||
|
this.nodeActors = new HashMap<>(); |
||||
|
this.nodeRoutes = new HashMap<>(); |
||||
|
this.service = systemContext.getRuleChainService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start(ActorContext context) throws Exception { |
||||
|
RuleChain ruleChain = service.findRuleChainById(entityId); |
||||
|
List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId); |
||||
|
// Creating and starting the actors;
|
||||
|
for (RuleNode ruleNode : ruleNodeList) { |
||||
|
ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode); |
||||
|
nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode)); |
||||
|
} |
||||
|
initRoutes(ruleChain, ruleNodeList); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onUpdate(ActorContext context) throws Exception { |
||||
|
RuleChain ruleChain = service.findRuleChainById(entityId); |
||||
|
List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId); |
||||
|
|
||||
|
for (RuleNode ruleNode : ruleNodeList) { |
||||
|
RuleNodeCtx existing = nodeActors.get(ruleNode.getId()); |
||||
|
if (existing == null) { |
||||
|
ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode); |
||||
|
nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode)); |
||||
|
} else { |
||||
|
existing.setSelf(ruleNode); |
||||
|
existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet()); |
||||
|
List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList()); |
||||
|
removedRules.forEach(ruleNodeId -> { |
||||
|
RuleNodeCtx removed = nodeActors.remove(ruleNodeId); |
||||
|
removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self); |
||||
|
}); |
||||
|
|
||||
|
initRoutes(ruleChain, ruleNodeList); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop(ActorContext context) throws Exception { |
||||
|
nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(context::stop); |
||||
|
nodeActors.clear(); |
||||
|
nodeRoutes.clear(); |
||||
|
context.stop(self); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onClusterEventMsg(ClusterEventMsg msg) throws Exception { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private ActorRef createRuleNodeActor(ActorContext context, RuleNode ruleNode) { |
||||
|
String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ? |
||||
|
DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME; |
||||
|
return context.actorOf( |
||||
|
Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId())) |
||||
|
.withDispatcher(dispatcherName), ruleNode.getId().toString()); |
||||
|
} |
||||
|
|
||||
|
private void initRoutes(RuleChain ruleChain, List<RuleNode> ruleNodeList) { |
||||
|
nodeRoutes.clear(); |
||||
|
// Populating the routes map;
|
||||
|
for (RuleNode ruleNode : ruleNodeList) { |
||||
|
List<EntityRelation> relations = service.getRuleNodeRelations(ruleNode.getId()); |
||||
|
for (EntityRelation relation : relations) { |
||||
|
if (relation.getTo().getEntityType() == EntityType.RULE_NODE) { |
||||
|
RuleNodeCtx ruleNodeCtx = nodeActors.get(new RuleNodeId(relation.getTo().getId())); |
||||
|
if (ruleNodeCtx == null) { |
||||
|
throw new IllegalArgumentException("Rule Node [" + relation.getFrom() + "] has invalid relation to Rule node [" + relation.getTo() + "]"); |
||||
|
} |
||||
|
} |
||||
|
nodeRoutes.computeIfAbsent(ruleNode.getId(), k -> new ArrayList<>()) |
||||
|
.add(new RuleNodeRelation(ruleNode.getId(), relation.getTo(), relation.getType())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
firstId = ruleChain.getFirstRuleNodeId(); |
||||
|
firstNode = nodeActors.get(ruleChain.getFirstRuleNodeId()); |
||||
|
state = ComponentLifecycleState.ACTIVE; |
||||
|
} |
||||
|
|
||||
|
void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) { |
||||
|
checkActive(); |
||||
|
TbMsg tbMsg = envelope.getTbMsg(); |
||||
|
//TODO: push to queue and act on ack in async way
|
||||
|
pushMsgToNode(firstNode, tbMsg); |
||||
|
} |
||||
|
|
||||
|
void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) { |
||||
|
checkActive(); |
||||
|
RuleNodeId originator = envelope.getOriginator(); |
||||
|
String targetRelationType = envelope.getRelationType(); |
||||
|
List<RuleNodeRelation> relations = nodeRoutes.get(originator); |
||||
|
if (relations == null) { |
||||
|
return; |
||||
|
} |
||||
|
boolean copy = relations.size() > 1; |
||||
|
for (RuleNodeRelation relation : relations) { |
||||
|
TbMsg msg = envelope.getMsg(); |
||||
|
if (copy) { |
||||
|
msg = msg.copy(); |
||||
|
} |
||||
|
if (targetRelationType == null || targetRelationType.equalsIgnoreCase(relation.getType())) { |
||||
|
switch (relation.getOut().getEntityType()) { |
||||
|
case RULE_NODE: |
||||
|
RuleNodeId targetRuleNodeId = new RuleNodeId(relation.getOut().getId()); |
||||
|
RuleNodeCtx targetRuleNode = nodeActors.get(targetRuleNodeId); |
||||
|
pushMsgToNode(targetRuleNode, msg); |
||||
|
break; |
||||
|
case RULE_CHAIN: |
||||
|
// TODO: implement
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg) { |
||||
|
if (nodeCtx != null) { |
||||
|
nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg), self); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.ActorRef; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.service.ContextAwareActor; |
||||
|
import org.thingsboard.server.actors.shared.plugin.PluginManager; |
||||
|
import org.thingsboard.server.actors.shared.rulechain.RuleChainManager; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.PluginId; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 15.03.18. |
||||
|
*/ |
||||
|
public abstract class RuleChainManagerActor extends ContextAwareActor { |
||||
|
|
||||
|
protected final RuleChainManager ruleChainManager; |
||||
|
protected final PluginManager pluginManager; |
||||
|
protected final RuleChainService ruleChainService; |
||||
|
|
||||
|
public RuleChainManagerActor(ActorSystemContext systemContext, RuleChainManager ruleChainManager, PluginManager pluginManager) { |
||||
|
super(systemContext); |
||||
|
this.ruleChainManager = ruleChainManager; |
||||
|
this.pluginManager = pluginManager; |
||||
|
this.ruleChainService = systemContext.getRuleChainService(); |
||||
|
} |
||||
|
|
||||
|
protected void initRuleChains() { |
||||
|
pluginManager.init(this.context()); |
||||
|
ruleChainManager.init(this.context()); |
||||
|
} |
||||
|
|
||||
|
protected ActorRef getEntityActorRef(EntityId entityId) { |
||||
|
ActorRef target = null; |
||||
|
switch (entityId.getEntityType()) { |
||||
|
case PLUGIN: |
||||
|
target = pluginManager.getOrCreateActor(this.context(), (PluginId) entityId); |
||||
|
break; |
||||
|
case RULE_CHAIN: |
||||
|
target = ruleChainManager.getOrCreateActor(this.context(), (RuleChainId) entityId); |
||||
|
break; |
||||
|
} |
||||
|
return target; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.service.ComponentActor; |
||||
|
import org.thingsboard.server.actors.service.ContextBasedCreator; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.msg.TbActorMsg; |
||||
|
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
||||
|
|
||||
|
public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> { |
||||
|
|
||||
|
private final RuleChainId ruleChainId; |
||||
|
|
||||
|
private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { |
||||
|
super(systemContext, tenantId, ruleNodeId); |
||||
|
this.ruleChainId = ruleChainId; |
||||
|
setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext, |
||||
|
logger, context().parent(), context().self())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean process(TbActorMsg msg) { |
||||
|
switch (msg.getMsgType()) { |
||||
|
case COMPONENT_LIFE_CYCLE_MSG: |
||||
|
onComponentLifecycleMsg((ComponentLifecycleMsg) msg); |
||||
|
break; |
||||
|
case RULE_CHAIN_TO_RULE_MSG: |
||||
|
onRuleChainToRuleNodeMsg((RuleChainToRuleNodeMsg) msg); |
||||
|
break; |
||||
|
case RULE_TO_SELF_ERROR_MSG: |
||||
|
onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg); |
||||
|
break; |
||||
|
case RULE_TO_SELF_MSG: |
||||
|
onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg); |
||||
|
break; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) { |
||||
|
logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg()); |
||||
|
try { |
||||
|
processor.onRuleToSelfMsg(msg); |
||||
|
increaseMessagesProcessedCount(); |
||||
|
} catch (Exception e) { |
||||
|
logAndPersist("onRuleMsg", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) { |
||||
|
logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg()); |
||||
|
try { |
||||
|
processor.onRuleChainToRuleNodeMsg(msg); |
||||
|
increaseMessagesProcessedCount(); |
||||
|
} catch (Exception e) { |
||||
|
logAndPersist("onRuleMsg", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void onRuleNodeToSelfErrorMsg(RuleNodeToSelfErrorMsg msg) { |
||||
|
logAndPersist("onRuleMsg", ActorSystemContext.toException(msg.getError())); |
||||
|
} |
||||
|
|
||||
|
public static class ActorCreator extends ContextBasedCreator<RuleNodeActor> { |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final RuleChainId ruleChainId; |
||||
|
private final RuleNodeId ruleNodeId; |
||||
|
|
||||
|
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { |
||||
|
super(context); |
||||
|
this.tenantId = tenantId; |
||||
|
this.ruleChainId = ruleChainId; |
||||
|
this.ruleNodeId = ruleNodeId; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public RuleNodeActor create() throws Exception { |
||||
|
return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected long getErrorPersistFrequency() { |
||||
|
return systemContext.getRuleNodeErrorPersistFrequency(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.ActorContext; |
||||
|
import akka.actor.ActorRef; |
||||
|
import akka.event.LoggingAdapter; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; |
||||
|
import org.thingsboard.server.common.data.rule.RuleNode; |
||||
|
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
|
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
|
||||
|
/** |
||||
|
* @author Andrew Shvayka |
||||
|
*/ |
||||
|
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { |
||||
|
|
||||
|
private final ActorRef parent; |
||||
|
private final ActorRef self; |
||||
|
private final RuleChainService service; |
||||
|
private RuleNode ruleNode; |
||||
|
private TbNode tbNode; |
||||
|
private TbContext defaultCtx; |
||||
|
|
||||
|
RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext |
||||
|
, LoggingAdapter logger, ActorRef parent, ActorRef self) { |
||||
|
super(systemContext, logger, tenantId, ruleNodeId); |
||||
|
this.parent = parent; |
||||
|
this.self = self; |
||||
|
this.service = systemContext.getRuleChainService(); |
||||
|
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId); |
||||
|
this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start(ActorContext context) throws Exception { |
||||
|
tbNode = initComponent(ruleNode); |
||||
|
state = ComponentLifecycleState.ACTIVE; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onUpdate(ActorContext context) throws Exception { |
||||
|
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(entityId); |
||||
|
boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType()) |
||||
|
&& ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); |
||||
|
this.ruleNode = newRuleNode; |
||||
|
this.defaultCtx.updateSelf(newRuleNode); |
||||
|
if (restartRequired) { |
||||
|
if (tbNode != null) { |
||||
|
tbNode.destroy(); |
||||
|
} |
||||
|
start(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop(ActorContext context) throws Exception { |
||||
|
if (tbNode != null) { |
||||
|
tbNode.destroy(); |
||||
|
} |
||||
|
context.stop(self); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onClusterEventMsg(ClusterEventMsg msg) throws Exception { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception { |
||||
|
checkActive(); |
||||
|
if (ruleNode.isDebugMode()) { |
||||
|
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg()); |
||||
|
} |
||||
|
tbNode.onMsg(defaultCtx, msg.getMsg()); |
||||
|
} |
||||
|
|
||||
|
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { |
||||
|
checkActive(); |
||||
|
if (ruleNode.isDebugMode()) { |
||||
|
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg()); |
||||
|
} |
||||
|
tbNode.onMsg(msg.getCtx(), msg.getMsg()); |
||||
|
} |
||||
|
|
||||
|
private TbNode initComponent(RuleNode ruleNode) throws Exception { |
||||
|
Class<?> componentClazz = Class.forName(ruleNode.getType()); |
||||
|
TbNode tbNode = (TbNode) (componentClazz.newInstance()); |
||||
|
tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration())); |
||||
|
return tbNode; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import akka.actor.ActorRef; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.rule.RuleNode; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.03.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
final class RuleNodeCtx { |
||||
|
private final TenantId tenantId; |
||||
|
private final ActorRef chainActor; |
||||
|
private final ActorRef selfActor; |
||||
|
private RuleNode self; |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.msg.MsgType; |
||||
|
import org.thingsboard.server.common.msg.TbActorMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.03.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg { |
||||
|
|
||||
|
private final RuleNodeId originator; |
||||
|
private final String relationType; |
||||
|
private final TbMsg msg; |
||||
|
|
||||
|
@Override |
||||
|
public MsgType getMsgType() { |
||||
|
return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.msg.MsgType; |
||||
|
import org.thingsboard.server.common.msg.TbActorMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.03.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
final class RuleNodeToSelfErrorMsg implements TbActorMsg { |
||||
|
|
||||
|
private final TbMsg msg; |
||||
|
private final Throwable error; |
||||
|
|
||||
|
@Override |
||||
|
public MsgType getMsgType() { |
||||
|
return MsgType.RULE_TO_SELF_ERROR_MSG; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.ruleChain; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.msg.MsgType; |
||||
|
import org.thingsboard.server.common.msg.TbActorMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.03.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
final class RuleNodeToSelfMsg implements TbActorMsg { |
||||
|
|
||||
|
private final TbMsg msg; |
||||
|
|
||||
|
@Override |
||||
|
public MsgType getMsgType() { |
||||
|
return MsgType.RULE_TO_SELF_MSG; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.shared; |
||||
|
|
||||
|
import akka.actor.ActorContext; |
||||
|
import akka.actor.ActorRef; |
||||
|
import akka.actor.Props; |
||||
|
import akka.actor.UntypedActor; |
||||
|
import akka.japi.Creator; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.service.ContextAwareActor; |
||||
|
import org.thingsboard.server.common.data.SearchTextBased; |
||||
|
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UUIDBased; |
||||
|
import org.thingsboard.server.common.data.page.PageDataIterable; |
||||
|
import org.thingsboard.server.common.data.plugin.PluginMetaData; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 15.03.18. |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public abstract class EntityActorsManager<T extends EntityId, A extends UntypedActor, M extends SearchTextBased<? extends UUIDBased>> { |
||||
|
|
||||
|
protected final ActorSystemContext systemContext; |
||||
|
protected final Map<T, ActorRef> actors; |
||||
|
|
||||
|
public EntityActorsManager(ActorSystemContext systemContext) { |
||||
|
this.systemContext = systemContext; |
||||
|
this.actors = new HashMap<>(); |
||||
|
} |
||||
|
|
||||
|
protected abstract TenantId getTenantId(); |
||||
|
|
||||
|
protected abstract String getDispatcherName(); |
||||
|
|
||||
|
protected abstract Creator<A> creator(T entityId); |
||||
|
|
||||
|
protected abstract PageDataIterable.FetchFunction<M> getFetchEntitiesFunction(); |
||||
|
|
||||
|
public void init(ActorContext context) { |
||||
|
for (M entity : new PageDataIterable<>(getFetchEntitiesFunction(), ContextAwareActor.ENTITY_PACK_LIMIT)) { |
||||
|
T entityId = (T) entity.getId(); |
||||
|
log.debug("[{}|{}] Creating entity actor", entityId.getEntityType(), entityId.getId()); |
||||
|
//TODO: remove this cast making UUIDBased subclass of EntityId an interface and vice versa.
|
||||
|
ActorRef actorRef = getOrCreateActor(context, entityId); |
||||
|
visit(entity, actorRef); |
||||
|
log.debug("[{}|{}] Entity actor created.", entityId.getEntityType(), entityId.getId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void visit(M entity, ActorRef actorRef) {} |
||||
|
|
||||
|
public ActorRef getOrCreateActor(ActorContext context, T entityId) { |
||||
|
return actors.computeIfAbsent(entityId, eId -> |
||||
|
context.actorOf(Props.create(creator(eId)) |
||||
|
.withDispatcher(getDispatcherName()), eId.toString())); |
||||
|
} |
||||
|
|
||||
|
public void broadcast(Object msg) { |
||||
|
actors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender())); |
||||
|
} |
||||
|
|
||||
|
public void remove(T id) { |
||||
|
actors.remove(id); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,135 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.shared.rule; |
|
||||
|
|
||||
import akka.actor.ActorContext; |
|
||||
import akka.actor.ActorRef; |
|
||||
import akka.actor.Props; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.thingsboard.server.actors.ActorSystemContext; |
|
||||
import org.thingsboard.server.actors.rule.RuleActor; |
|
||||
import org.thingsboard.server.actors.rule.RuleActorChain; |
|
||||
import org.thingsboard.server.actors.rule.RuleActorMetaData; |
|
||||
import org.thingsboard.server.actors.rule.SimpleRuleActorChain; |
|
||||
import org.thingsboard.server.actors.service.ContextAwareActor; |
|
||||
import org.thingsboard.server.common.data.id.RuleId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.common.data.page.PageDataIterable; |
|
||||
import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction; |
|
||||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|
||||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; |
|
||||
import org.thingsboard.server.common.data.rule.RuleMetaData; |
|
||||
import org.thingsboard.server.dao.rule.RuleService; |
|
||||
|
|
||||
import java.util.*; |
|
||||
|
|
||||
@Slf4j |
|
||||
public abstract class RuleManager { |
|
||||
|
|
||||
protected final ActorSystemContext systemContext; |
|
||||
protected final RuleService ruleService; |
|
||||
protected final Map<RuleId, ActorRef> ruleActors; |
|
||||
protected final TenantId tenantId; |
|
||||
|
|
||||
private Map<RuleMetaData, RuleActorMetaData> ruleMap; |
|
||||
private RuleActorChain ruleChain; |
|
||||
|
|
||||
public RuleManager(ActorSystemContext systemContext, TenantId tenantId) { |
|
||||
this.systemContext = systemContext; |
|
||||
this.ruleService = systemContext.getRuleService(); |
|
||||
this.ruleActors = new HashMap<>(); |
|
||||
this.tenantId = tenantId; |
|
||||
} |
|
||||
|
|
||||
public void init(ActorContext context) { |
|
||||
doInit(context); |
|
||||
} |
|
||||
|
|
||||
private void doInit(ActorContext context) { |
|
||||
PageDataIterable<RuleMetaData> ruleIterator = new PageDataIterable<>(getFetchRulesFunction(), |
|
||||
ContextAwareActor.ENTITY_PACK_LIMIT); |
|
||||
ruleMap = new HashMap<>(); |
|
||||
|
|
||||
for (RuleMetaData rule : ruleIterator) { |
|
||||
log.debug("[{}] Creating rule actor {}", rule.getId(), rule); |
|
||||
ActorRef ref = getOrCreateRuleActor(context, rule.getId()); |
|
||||
ruleMap.put(rule, RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref)); |
|
||||
log.debug("[{}] Rule actor created.", rule.getId()); |
|
||||
} |
|
||||
|
|
||||
refreshRuleChain(); |
|
||||
} |
|
||||
|
|
||||
public Optional<ActorRef> update(ActorContext context, RuleId ruleId, ComponentLifecycleEvent event) { |
|
||||
if (ruleMap == null) { |
|
||||
doInit(context); |
|
||||
} |
|
||||
RuleMetaData rule; |
|
||||
if (event != ComponentLifecycleEvent.DELETED) { |
|
||||
rule = systemContext.getRuleService().findRuleById(ruleId); |
|
||||
} else { |
|
||||
rule = ruleMap.keySet().stream() |
|
||||
.filter(r -> r.getId().equals(ruleId)) |
|
||||
.peek(r -> r.setState(ComponentLifecycleState.SUSPENDED)) |
|
||||
.findFirst() |
|
||||
.orElse(null); |
|
||||
if (rule != null) { |
|
||||
ruleMap.remove(rule); |
|
||||
ruleActors.remove(ruleId); |
|
||||
} |
|
||||
} |
|
||||
if (rule != null) { |
|
||||
RuleActorMetaData actorMd = ruleMap.get(rule); |
|
||||
if (actorMd == null) { |
|
||||
ActorRef ref = getOrCreateRuleActor(context, rule.getId()); |
|
||||
actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref); |
|
||||
ruleMap.put(rule, actorMd); |
|
||||
} |
|
||||
refreshRuleChain(); |
|
||||
return Optional.of(actorMd.getActorRef()); |
|
||||
} else { |
|
||||
log.warn("[{}] Can't process unknown rule!", ruleId); |
|
||||
return Optional.empty(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
abstract FetchFunction<RuleMetaData> getFetchRulesFunction(); |
|
||||
|
|
||||
abstract String getDispatcherName(); |
|
||||
|
|
||||
public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) { |
|
||||
return ruleActors.computeIfAbsent(ruleId, rId -> |
|
||||
context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, rId)) |
|
||||
.withDispatcher(getDispatcherName()), rId.toString())); |
|
||||
} |
|
||||
|
|
||||
public RuleActorChain getRuleChain(ActorContext context) { |
|
||||
if (ruleChain == null) { |
|
||||
doInit(context); |
|
||||
} |
|
||||
return ruleChain; |
|
||||
} |
|
||||
|
|
||||
private void refreshRuleChain() { |
|
||||
Set<RuleActorMetaData> activeRuleSet = new HashSet<>(); |
|
||||
for (Map.Entry<RuleMetaData, RuleActorMetaData> rule : ruleMap.entrySet()) { |
|
||||
if (rule.getKey().getState() == ComponentLifecycleState.ACTIVE) { |
|
||||
activeRuleSet.add(rule.getValue()); |
|
||||
} |
|
||||
} |
|
||||
ruleChain = new SimpleRuleActorChain(activeRuleSet); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,59 @@ |
|||||
|
/** |
||||
|
* 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.server.actors.shared.rulechain; |
||||
|
|
||||
|
import akka.actor.ActorRef; |
||||
|
import akka.japi.Creator; |
||||
|
import lombok.Getter; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.actors.ruleChain.RuleChainActor; |
||||
|
import org.thingsboard.server.actors.shared.EntityActorsManager; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.rule.RuleChain; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 15.03.18. |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public abstract class RuleChainManager extends EntityActorsManager<RuleChainId, RuleChainActor, RuleChain> { |
||||
|
|
||||
|
protected final RuleChainService service; |
||||
|
@Getter |
||||
|
protected RuleChain rootChain; |
||||
|
@Getter |
||||
|
protected ActorRef rootChainActor; |
||||
|
|
||||
|
public RuleChainManager(ActorSystemContext systemContext) { |
||||
|
super(systemContext); |
||||
|
this.service = systemContext.getRuleChainService(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Creator<RuleChainActor> creator(RuleChainId entityId) { |
||||
|
return new RuleChainActor.ActorCreator(systemContext, getTenantId(), entityId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void visit(RuleChain entity, ActorRef actorRef) { |
||||
|
if (entity.isRoot()) { |
||||
|
rootChain = entity; |
||||
|
rootChainActor = actorRef; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,40 +0,0 @@ |
|||||
/** |
|
||||
* 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.server.actors.tenant; |
|
||||
|
|
||||
import org.thingsboard.server.actors.rule.RuleActorChain; |
|
||||
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; |
|
||||
|
|
||||
public class RuleChainDeviceMsg { |
|
||||
|
|
||||
private final ToDeviceActorMsg toDeviceActorMsg; |
|
||||
private final RuleActorChain ruleChain; |
|
||||
|
|
||||
public RuleChainDeviceMsg(ToDeviceActorMsg toDeviceActorMsg, RuleActorChain ruleChain) { |
|
||||
super(); |
|
||||
this.toDeviceActorMsg = toDeviceActorMsg; |
|
||||
this.ruleChain = ruleChain; |
|
||||
} |
|
||||
|
|
||||
public ToDeviceActorMsg getToDeviceActorMsg() { |
|
||||
return toDeviceActorMsg; |
|
||||
} |
|
||||
|
|
||||
public RuleActorChain getRuleChain() { |
|
||||
return ruleChain; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -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.server.controller; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.web.context.request.async.DeferredResult; |
||||
|
import org.thingsboard.server.service.security.ValidationCallback; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 21.02.17. |
||||
|
*/ |
||||
|
public class HttpValidationCallback extends ValidationCallback<DeferredResult<ResponseEntity>> { |
||||
|
|
||||
|
public HttpValidationCallback(DeferredResult<ResponseEntity> response, FutureCallback<DeferredResult<ResponseEntity>> action) { |
||||
|
super(response, action); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,586 @@ |
|||||
|
/** |
||||
|
* 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.server.controller; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.google.common.base.Function; |
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestMethod; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.springframework.web.context.request.async.DeferredResult; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.audit.ActionType; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.UUIDBased; |
||||
|
import org.thingsboard.server.common.data.kv.Aggregation; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseTsKvQuery; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.KvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvQuery; |
||||
|
import org.thingsboard.server.common.msg.core.TelemetryUploadRequest; |
||||
|
import org.thingsboard.server.common.transport.adaptor.JsonConverter; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.extensions.api.exception.InvalidParametersException; |
||||
|
import org.thingsboard.server.extensions.api.exception.UncheckedApiException; |
||||
|
import org.thingsboard.server.extensions.api.plugins.PluginConstants; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.TsData; |
||||
|
import org.thingsboard.server.service.security.AccessValidator; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; |
||||
|
|
||||
|
import javax.annotation.Nullable; |
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 22.03.18. |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping(PluginConstants.TELEMETRY_URL_PREFIX) |
||||
|
@Slf4j |
||||
|
public class TelemetryController extends BaseController { |
||||
|
|
||||
|
@Autowired |
||||
|
private AttributesService attributesService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TimeseriesService tsService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TelemetrySubscriptionService tsSubService; |
||||
|
|
||||
|
@Autowired |
||||
|
private AccessValidator accessValidator; |
||||
|
|
||||
|
private ExecutorService executor; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
executor = Executors.newSingleThreadExecutor(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (executor != null) { |
||||
|
executor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getAttributeKeys( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getAttributeKeysByScope( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr |
||||
|
, @PathVariable("scope") String scope) throws ThingsboardException { |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> getAttributeKeysCallback(result, entityId, scope)); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getAttributes( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getAttributesByScope( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getTimeseriesKeys( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> { |
||||
|
Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getLatestTimeseries( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
|
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> getTimeseries( |
||||
|
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@RequestParam(name = "keys") String keys, |
||||
|
@RequestParam(name = "startTs") Long startTs, |
||||
|
@RequestParam(name = "endTs") Long endTs, |
||||
|
@RequestParam(name = "interval", defaultValue = "0") Long interval, |
||||
|
@RequestParam(name = "limit", defaultValue = "100") Integer limit, |
||||
|
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr |
||||
|
) throws ThingsboardException { |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, |
||||
|
(result, entityId) -> { |
||||
|
// If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
|
||||
|
Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); |
||||
|
List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg)) |
||||
|
.collect(Collectors.toList()); |
||||
|
|
||||
|
Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope, |
||||
|
@RequestBody JsonNode request) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); |
||||
|
return saveAttributes(entityId, scope, request); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestBody JsonNode request) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
||||
|
return saveAttributes(entityId, scope, request); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestBody JsonNode request) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
||||
|
return saveAttributes(entityId, scope, request); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestBody String requestBody) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
||||
|
return saveTelemetry(entityId, requestBody, 0L); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, @PathVariable("ttl") Long ttl, |
||||
|
@RequestBody String requestBody) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
||||
|
return saveTelemetry(entityId, requestBody, ttl); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestParam(name = "keys") String keysStr) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); |
||||
|
return deleteAttributes(entityId, scope, keysStr); |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) |
||||
|
@ResponseBody |
||||
|
public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
||||
|
@PathVariable("scope") String scope, |
||||
|
@RequestParam(name = "keys") String keysStr) throws ThingsboardException { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
||||
|
return deleteAttributes(entityId, scope, keysStr); |
||||
|
} |
||||
|
|
||||
|
private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException { |
||||
|
List<String> keys = toKeysList(keysStr); |
||||
|
if (keys.isEmpty()) { |
||||
|
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
if (DataConstants.SERVER_SCOPE.equals(scope) || |
||||
|
DataConstants.SHARED_SCOPE.equals(scope) || |
||||
|
DataConstants.CLIENT_SCOPE.equals(scope)) { |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> { |
||||
|
ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys); |
||||
|
Futures.addCallback(future, new FutureCallback<List<Void>>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable List<Void> tmp) { |
||||
|
logAttributesDeleted(user, entityId, scope, keys, null); |
||||
|
result.setResult(new ResponseEntity<>(HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
logAttributesDeleted(user, entityId, scope, keys, t); |
||||
|
result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); |
||||
|
} |
||||
|
}, executor); |
||||
|
}); |
||||
|
} else { |
||||
|
return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException { |
||||
|
if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) { |
||||
|
return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
if (json.isObject()) { |
||||
|
List<AttributeKvEntry> attributes = extractRequestAttributes(json); |
||||
|
if (attributes.isEmpty()) { |
||||
|
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> { |
||||
|
tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable Void tmp) { |
||||
|
logAttributesUpdated(user, entityId, scope, attributes, null); |
||||
|
result.setResult(new ResponseEntity(HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
logAttributesUpdated(user, entityId, scope, attributes, t); |
||||
|
AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException { |
||||
|
TelemetryUploadRequest telemetryRequest; |
||||
|
JsonElement telemetryJson; |
||||
|
try { |
||||
|
telemetryJson = new JsonParser().parse(requestBody); |
||||
|
} catch (Exception e) { |
||||
|
return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
try { |
||||
|
telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson); |
||||
|
} catch (Exception e) { |
||||
|
return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
List<TsKvEntry> entries = new ArrayList<>(); |
||||
|
for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) { |
||||
|
for (KvEntry kv : entry.getValue()) { |
||||
|
entries.add(new BasicTsKvEntry(entry.getKey(), kv)); |
||||
|
} |
||||
|
} |
||||
|
if (entries.isEmpty()) { |
||||
|
return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST); |
||||
|
} |
||||
|
SecurityUser user = getCurrentUser(); |
||||
|
return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> { |
||||
|
tsSubService.saveAndNotify(entityId, entries, ttl, new FutureCallback<Void>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable Void tmp) { |
||||
|
result.setResult(new ResponseEntity(HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) { |
||||
|
ListenableFuture<List<TsKvEntry>> future; |
||||
|
if (StringUtils.isEmpty(keys)) { |
||||
|
future = tsService.findAllLatest(entityId); |
||||
|
} else { |
||||
|
future = tsService.findLatest(entityId, toKeysList(keys)); |
||||
|
} |
||||
|
Futures.addCallback(future, getTsKvListCallback(result)); |
||||
|
} |
||||
|
|
||||
|
private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) { |
||||
|
List<String> keyList = toKeysList(keys); |
||||
|
FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); |
||||
|
if (!StringUtils.isEmpty(scope)) { |
||||
|
if (keyList != null && !keyList.isEmpty()) { |
||||
|
Futures.addCallback(attributesService.find(entityId, scope, keyList), callback); |
||||
|
} else { |
||||
|
Futures.addCallback(attributesService.findAll(entityId, scope), callback); |
||||
|
} |
||||
|
} else { |
||||
|
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>(); |
||||
|
for (String tmpScope : DataConstants.allScopes()) { |
||||
|
if (keyList != null && !keyList.isEmpty()) { |
||||
|
futures.add(attributesService.find(entityId, tmpScope, keyList)); |
||||
|
} else { |
||||
|
futures.add(attributesService.findAll(entityId, tmpScope)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures); |
||||
|
|
||||
|
Futures.addCallback(future, callback); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId, String scope) { |
||||
|
Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result)); |
||||
|
} |
||||
|
|
||||
|
private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId) { |
||||
|
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>(); |
||||
|
for (String scope : DataConstants.allScopes()) { |
||||
|
futures.add(attributesService.findAll(entityId, scope)); |
||||
|
} |
||||
|
|
||||
|
ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures); |
||||
|
|
||||
|
Futures.addCallback(future, getAttributeKeysToResponseCallback(result)); |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) { |
||||
|
return new FutureCallback<List<TsKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<TsKvEntry> values) { |
||||
|
List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList()); |
||||
|
response.setResult(new ResponseEntity<>(keys, HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error("Failed to fetch attributes", e); |
||||
|
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<List<AttributeKvEntry>> getAttributeKeysToResponseCallback(final DeferredResult<ResponseEntity> response) { |
||||
|
return new FutureCallback<List<AttributeKvEntry>>() { |
||||
|
|
||||
|
@Override |
||||
|
public void onSuccess(List<AttributeKvEntry> attributes) { |
||||
|
List<String> keys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); |
||||
|
response.setResult(new ResponseEntity<>(keys, HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error("Failed to fetch attributes", e); |
||||
|
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response, |
||||
|
final SecurityUser user, final String scope, |
||||
|
final EntityId entityId, final List<String> keyList) { |
||||
|
return new FutureCallback<List<AttributeKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<AttributeKvEntry> attributes) { |
||||
|
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(), |
||||
|
attribute.getKey(), attribute.getValue())).collect(Collectors.toList()); |
||||
|
logAttributesRead(user, entityId, scope, keyList, null); |
||||
|
response.setResult(new ResponseEntity<>(values, HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error("Failed to fetch attributes", e); |
||||
|
logAttributesRead(user, entityId, scope, keyList, e); |
||||
|
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) { |
||||
|
return new FutureCallback<List<TsKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<TsKvEntry> data) { |
||||
|
Map<String, List<TsData>> result = new LinkedHashMap<>(); |
||||
|
for (TsKvEntry entry : data) { |
||||
|
result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()) |
||||
|
.add(new TsData(entry.getTs(), entry.getValueAsString())); |
||||
|
} |
||||
|
response.setResult(new ResponseEntity<>(result, HttpStatus.OK)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error("Failed to fetch historical data", e); |
||||
|
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) { |
||||
|
auditLogService.logEntityAction( |
||||
|
user.getTenantId(), |
||||
|
user.getCustomerId(), |
||||
|
user.getId(), |
||||
|
user.getName(), |
||||
|
(UUIDBased & EntityId) entityId, |
||||
|
null, |
||||
|
ActionType.ATTRIBUTES_DELETED, |
||||
|
toException(e), |
||||
|
scope, |
||||
|
keys); |
||||
|
} |
||||
|
|
||||
|
private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) { |
||||
|
auditLogService.logEntityAction( |
||||
|
user.getTenantId(), |
||||
|
user.getCustomerId(), |
||||
|
user.getId(), |
||||
|
user.getName(), |
||||
|
(UUIDBased & EntityId) entityId, |
||||
|
null, |
||||
|
ActionType.ATTRIBUTES_UPDATED, |
||||
|
toException(e), |
||||
|
scope, |
||||
|
attributes); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) { |
||||
|
auditLogService.logEntityAction( |
||||
|
user.getTenantId(), |
||||
|
user.getCustomerId(), |
||||
|
user.getId(), |
||||
|
user.getName(), |
||||
|
(UUIDBased & EntityId) entityId, |
||||
|
null, |
||||
|
ActionType.ATTRIBUTES_READ, |
||||
|
toException(e), |
||||
|
scope, |
||||
|
keys); |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) { |
||||
|
return Futures.transform(Futures.successfulAsList(futures), |
||||
|
(Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> { |
||||
|
List<AttributeKvEntry> tmp = new ArrayList<>(); |
||||
|
if (input != null) { |
||||
|
input.forEach(tmp::addAll); |
||||
|
} |
||||
|
return tmp; |
||||
|
}, executor); |
||||
|
} |
||||
|
|
||||
|
private List<String> toKeysList(String keys) { |
||||
|
List<String> keyList = null; |
||||
|
if (!StringUtils.isEmpty(keys)) { |
||||
|
keyList = Arrays.asList(keys.split(",")); |
||||
|
} |
||||
|
return keyList; |
||||
|
} |
||||
|
|
||||
|
private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) { |
||||
|
DeferredResult<ResponseEntity> result = new DeferredResult<>(); |
||||
|
result.setResult(new ResponseEntity<>(message, status)); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) { |
||||
|
long ts = System.currentTimeMillis(); |
||||
|
List<AttributeKvEntry> attributes = new ArrayList<>(); |
||||
|
jsonNode.fields().forEachRemaining(entry -> { |
||||
|
String key = entry.getKey(); |
||||
|
JsonNode value = entry.getValue(); |
||||
|
if (entry.getValue().isTextual()) { |
||||
|
attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts)); |
||||
|
} else if (entry.getValue().isBoolean()) { |
||||
|
attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts)); |
||||
|
} else if (entry.getValue().isDouble()) { |
||||
|
attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts)); |
||||
|
} else if (entry.getValue().isNumber()) { |
||||
|
if (entry.getValue().isBigInteger()) { |
||||
|
throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!")); |
||||
|
} else { |
||||
|
attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts)); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
return attributes; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
/** |
||||
|
* 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.server.service.executors; |
||||
|
|
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.common.util.concurrent.ListeningExecutorService; |
||||
|
import com.google.common.util.concurrent.MoreExecutors; |
||||
|
import org.thingsboard.rule.engine.api.ListeningExecutor; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.concurrent.Callable; |
||||
|
import java.util.concurrent.Executors; |
||||
|
|
||||
|
/** |
||||
|
* Created by igor on 4/13/18. |
||||
|
*/ |
||||
|
public abstract class AbstractListeningExecutor implements ListeningExecutor { |
||||
|
|
||||
|
private ListeningExecutorService service; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
this.service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(getThreadPollSize())); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void destroy() { |
||||
|
if (this.service != null) { |
||||
|
this.service.shutdown(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T> ListenableFuture<T> executeAsync(Callable<T> task) { |
||||
|
return service.submit(task); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(Runnable command) { |
||||
|
service.execute(command); |
||||
|
} |
||||
|
|
||||
|
protected abstract int getThreadPollSize(); |
||||
|
|
||||
|
} |
||||
@ -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.server.service.executors; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
@Component |
||||
|
public class DbCallbackExecutorService extends AbstractListeningExecutor { |
||||
|
|
||||
|
@Value("${actors.rule.db_callback_thread_pool_size}") |
||||
|
private int dbCallbackExecutorThreadPoolSize; |
||||
|
|
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return dbCallbackExecutorThreadPoolSize; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.server.service.mail; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.service.executors.AbstractListeningExecutor; |
||||
|
|
||||
|
@Component |
||||
|
public class MailExecutorService extends AbstractListeningExecutor { |
||||
|
|
||||
|
@Value("${actors.rule.mail_thread_pool_size}") |
||||
|
private int mailExecutorThreadPoolSize; |
||||
|
|
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return mailExecutorThreadPoolSize; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.server.service.script; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.service.executors.AbstractListeningExecutor; |
||||
|
|
||||
|
@Component |
||||
|
public class JsExecutorService extends AbstractListeningExecutor { |
||||
|
|
||||
|
@Value("${actors.rule.js_thread_pool_size}") |
||||
|
private int jsExecutorThreadPoolSize; |
||||
|
|
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return jsExecutorThreadPoolSize; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,211 @@ |
|||||
|
/** |
||||
|
* 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.server.service.script; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.google.common.collect.Sets; |
||||
|
import jdk.nashorn.api.scripting.NashornScriptEngineFactory; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import javax.script.Invocable; |
||||
|
import javax.script.ScriptEngine; |
||||
|
import javax.script.ScriptException; |
||||
|
import java.util.Collections; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
|
||||
|
@Slf4j |
||||
|
public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEngine { |
||||
|
|
||||
|
public static final String MSG = "msg"; |
||||
|
public static final String METADATA = "metadata"; |
||||
|
public static final String MSG_TYPE = "msgType"; |
||||
|
|
||||
|
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " + |
||||
|
" var msg = JSON.parse(msgStr); " + |
||||
|
" var metadata = JSON.parse(metadataStr); " + |
||||
|
" return JSON.stringify(%s(msg, metadata, msgType));" + |
||||
|
" function %s(%s, %s, %s) {"; |
||||
|
private static final String JS_WRAPPER_SUFFIX = "}" + |
||||
|
"\n}"; |
||||
|
|
||||
|
private static final ObjectMapper mapper = new ObjectMapper(); |
||||
|
private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); |
||||
|
private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"}); |
||||
|
|
||||
|
private final String invokeFunctionName; |
||||
|
|
||||
|
public NashornJsEngine(String script, String functionName, String... argNames) { |
||||
|
this.invokeFunctionName = "invokeInternal" + this.hashCode(); |
||||
|
String msgArg; |
||||
|
String metadataArg; |
||||
|
String msgTypeArg; |
||||
|
if (argNames != null && argNames.length == 3) { |
||||
|
msgArg = argNames[0]; |
||||
|
metadataArg = argNames[1]; |
||||
|
msgTypeArg = argNames[2]; |
||||
|
} else { |
||||
|
msgArg = MSG; |
||||
|
metadataArg = METADATA; |
||||
|
msgTypeArg = MSG_TYPE; |
||||
|
} |
||||
|
String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, this.invokeFunctionName, |
||||
|
functionName, functionName, msgArg, metadataArg, msgTypeArg); |
||||
|
compileScript(jsWrapperPrefix + script + JS_WRAPPER_SUFFIX); |
||||
|
} |
||||
|
|
||||
|
private void compileScript(String script) { |
||||
|
try { |
||||
|
engine.eval(script); |
||||
|
} catch (ScriptException e) { |
||||
|
log.warn("Failed to compile JS script: {}", e.getMessage(), e); |
||||
|
throw new IllegalArgumentException("Can't compile script: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static String[] prepareArgs(TbMsg msg) { |
||||
|
try { |
||||
|
String[] args = new String[3]; |
||||
|
if (msg.getData() != null) { |
||||
|
args[0] = msg.getData(); |
||||
|
} else { |
||||
|
args[0] = ""; |
||||
|
} |
||||
|
args[1] = mapper.writeValueAsString(msg.getMetaData().getData()); |
||||
|
args[2] = msg.getType(); |
||||
|
return args; |
||||
|
} catch (Throwable th) { |
||||
|
throw new IllegalArgumentException("Cannot bind js args", th); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) { |
||||
|
try { |
||||
|
String data = null; |
||||
|
Map<String, String> metadata = null; |
||||
|
String messageType = null; |
||||
|
if (msgData.has(MSG)) { |
||||
|
JsonNode msgPayload = msgData.get(MSG); |
||||
|
data = mapper.writeValueAsString(msgPayload); |
||||
|
} |
||||
|
if (msgData.has(METADATA)) { |
||||
|
JsonNode msgMetadata = msgData.get(METADATA); |
||||
|
metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() { |
||||
|
}); |
||||
|
} |
||||
|
if (msgData.has(MSG_TYPE)) { |
||||
|
messageType = msgData.get(MSG_TYPE).asText(); |
||||
|
} |
||||
|
String newData = data != null ? data : msg.getData(); |
||||
|
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData(); |
||||
|
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); |
||||
|
return new TbMsg(msg.getId(), newMessageType, msg.getOriginator(), newMetadata, newData); |
||||
|
} catch (Throwable th) { |
||||
|
th.printStackTrace(); |
||||
|
throw new RuntimeException("Failed to unbind message data from javascript result", th); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbMsg executeUpdate(TbMsg msg) throws ScriptException { |
||||
|
JsonNode result = executeScript(msg); |
||||
|
if (!result.isObject()) { |
||||
|
log.warn("Wrong result type: {}", result.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + result.getNodeType()); |
||||
|
} |
||||
|
return unbindMsg(result, msg); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException { |
||||
|
JsonNode result = executeScript(prevMsg); |
||||
|
if (!result.isObject()) { |
||||
|
log.warn("Wrong result type: {}", result.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + result.getNodeType()); |
||||
|
} |
||||
|
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); |
||||
|
if (!result.isTextual()) { |
||||
|
log.warn("Wrong result type: {}", result.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + result.getNodeType()); |
||||
|
} |
||||
|
return result.asText(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean executeFilter(TbMsg msg) throws ScriptException { |
||||
|
JsonNode result = executeScript(msg); |
||||
|
if (!result.isBoolean()) { |
||||
|
log.warn("Wrong result type: {}", result.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + result.getNodeType()); |
||||
|
} |
||||
|
return result.asBoolean(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Set<String> executeSwitch(TbMsg msg) throws ScriptException { |
||||
|
JsonNode result = executeScript(msg); |
||||
|
if (result.isTextual()) { |
||||
|
return Collections.singleton(result.asText()); |
||||
|
} else if (result.isArray()) { |
||||
|
Set<String> nextStates = Sets.newHashSet(); |
||||
|
for (JsonNode val : result) { |
||||
|
if (!val.isTextual()) { |
||||
|
log.warn("Wrong result type: {}", val.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + val.getNodeType()); |
||||
|
} else { |
||||
|
nextStates.add(val.asText()); |
||||
|
} |
||||
|
} |
||||
|
return nextStates; |
||||
|
} else { |
||||
|
log.warn("Wrong result type: {}", result.getNodeType()); |
||||
|
throw new ScriptException("Wrong result type: " + result.getNodeType()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private JsonNode executeScript(TbMsg msg) throws ScriptException { |
||||
|
try { |
||||
|
String[] inArgs = prepareArgs(msg); |
||||
|
String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); |
||||
|
return mapper.readTree(eval); |
||||
|
} catch (ScriptException | IllegalArgumentException th) { |
||||
|
throw th; |
||||
|
} catch (Throwable th) { |
||||
|
th.printStackTrace(); |
||||
|
throw new RuntimeException("Failed to execute js script", th); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void destroy() { |
||||
|
engine = null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,295 @@ |
|||||
|
/** |
||||
|
* 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.server.service.security; |
||||
|
|
||||
|
import com.google.common.base.Function; |
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.springframework.web.context.request.async.DeferredResult; |
||||
|
import org.thingsboard.server.actors.plugin.ValidationResult; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.rule.RuleChain; |
||||
|
import org.thingsboard.server.controller.HttpValidationCallback; |
||||
|
import org.thingsboard.server.dao.alarm.AlarmService; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.rule.RuleChainService; |
||||
|
import org.thingsboard.server.dao.tenant.TenantService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.extensions.api.exception.ToErrorResponseEntity; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
import javax.annotation.Nullable; |
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.function.BiConsumer; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
@Component |
||||
|
public class AccessValidator { |
||||
|
|
||||
|
public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; |
||||
|
public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; |
||||
|
public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; |
||||
|
|
||||
|
@Autowired |
||||
|
protected TenantService tenantService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected CustomerService customerService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected UserService userService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected DeviceService deviceService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected AssetService assetService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected AlarmService alarmService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected RuleChainService ruleChainService; |
||||
|
|
||||
|
private ExecutorService executor; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
executor = Executors.newSingleThreadExecutor(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (executor != null) { |
||||
|
executor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException { |
||||
|
return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); |
||||
|
} |
||||
|
|
||||
|
public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException { |
||||
|
return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr), |
||||
|
onSuccess, onFailure); |
||||
|
} |
||||
|
|
||||
|
public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException { |
||||
|
return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); |
||||
|
} |
||||
|
|
||||
|
public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess, |
||||
|
BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException { |
||||
|
|
||||
|
final DeferredResult<ResponseEntity> response = new DeferredResult<>(); |
||||
|
|
||||
|
validate(currentUser, entityId, new HttpValidationCallback(response, |
||||
|
new FutureCallback<DeferredResult<ResponseEntity>>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) { |
||||
|
onSuccess.accept(response, entityId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
onFailure.accept(response, t); |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
public <T> void validate(SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
switch (entityId.getEntityType()) { |
||||
|
case DEVICE: |
||||
|
validateDevice(currentUser, entityId, callback); |
||||
|
return; |
||||
|
case ASSET: |
||||
|
validateAsset(currentUser, entityId, callback); |
||||
|
return; |
||||
|
case RULE_CHAIN: |
||||
|
validateRuleChain(currentUser, entityId, callback); |
||||
|
return; |
||||
|
case CUSTOMER: |
||||
|
validateCustomer(currentUser, entityId, callback); |
||||
|
return; |
||||
|
case TENANT: |
||||
|
validateTenant(currentUser, entityId, callback); |
||||
|
return; |
||||
|
default: |
||||
|
//TODO: add support of other entities
|
||||
|
throw new IllegalStateException("Not Implemented!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void validateDevice(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
if (currentUser.isSystemAdmin()) { |
||||
|
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); |
||||
|
} else { |
||||
|
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); |
||||
|
Futures.addCallback(deviceFuture, getCallback(callback, device -> { |
||||
|
if (device == null) { |
||||
|
return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); |
||||
|
} else { |
||||
|
if (!device.getTenantId().equals(currentUser.getTenantId())) { |
||||
|
return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!"); |
||||
|
} else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) { |
||||
|
return ValidationResult.accessDenied("Device doesn't belong to the current Customer!"); |
||||
|
} else { |
||||
|
return ValidationResult.ok(); |
||||
|
} |
||||
|
} |
||||
|
}), executor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <T> void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
if (currentUser.isSystemAdmin()) { |
||||
|
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); |
||||
|
} else { |
||||
|
ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId())); |
||||
|
Futures.addCallback(assetFuture, getCallback(callback, asset -> { |
||||
|
if (asset == null) { |
||||
|
return ValidationResult.entityNotFound("Asset with requested id wasn't found!"); |
||||
|
} else { |
||||
|
if (!asset.getTenantId().equals(currentUser.getTenantId())) { |
||||
|
return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!"); |
||||
|
} else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) { |
||||
|
return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!"); |
||||
|
} else { |
||||
|
return ValidationResult.ok(); |
||||
|
} |
||||
|
} |
||||
|
}), executor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private <T> void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
if (currentUser.isCustomerUser()) { |
||||
|
callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); |
||||
|
} else { |
||||
|
ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId())); |
||||
|
Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> { |
||||
|
if (ruleChain == null) { |
||||
|
return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!"); |
||||
|
} else { |
||||
|
if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) { |
||||
|
return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!"); |
||||
|
} else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) { |
||||
|
return ValidationResult.accessDenied("Rule chain is not in system scope!"); |
||||
|
} else { |
||||
|
return ValidationResult.ok(); |
||||
|
} |
||||
|
} |
||||
|
}), executor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <T> void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
if (currentUser.isSystemAdmin()) { |
||||
|
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); |
||||
|
} else { |
||||
|
ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId())); |
||||
|
Futures.addCallback(customerFuture, getCallback(callback, customer -> { |
||||
|
if (customer == null) { |
||||
|
return ValidationResult.entityNotFound("Customer with requested id wasn't found!"); |
||||
|
} else { |
||||
|
if (!customer.getTenantId().equals(currentUser.getTenantId())) { |
||||
|
return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!"); |
||||
|
} else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) { |
||||
|
return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!"); |
||||
|
} else { |
||||
|
return ValidationResult.ok(); |
||||
|
} |
||||
|
} |
||||
|
}), executor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <T> void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) { |
||||
|
if (currentUser.isCustomerUser()) { |
||||
|
callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); |
||||
|
} else if (currentUser.isSystemAdmin()) { |
||||
|
callback.onSuccess(ValidationResult.ok()); |
||||
|
} else { |
||||
|
ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); |
||||
|
Futures.addCallback(tenantFuture, getCallback(callback, tenant -> { |
||||
|
if (tenant == null) { |
||||
|
return ValidationResult.entityNotFound("Tenant with requested id wasn't found!"); |
||||
|
} else if (!tenant.getId().equals(currentUser.getTenantId())) { |
||||
|
return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!"); |
||||
|
} else { |
||||
|
return ValidationResult.ok(); |
||||
|
} |
||||
|
}), executor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <T> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult> transformer) { |
||||
|
return new FutureCallback<T>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable T result) { |
||||
|
callback.onSuccess(transformer.apply(result)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) { |
||||
|
ResponseEntity responseEntity; |
||||
|
if (e != null && e instanceof ToErrorResponseEntity) { |
||||
|
responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); |
||||
|
} else if (e != null && e instanceof IllegalArgumentException) { |
||||
|
responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); |
||||
|
} else { |
||||
|
responseEntity = new ResponseEntity<>(defaultErrorStatus); |
||||
|
} |
||||
|
response.setResult(responseEntity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
/** |
||||
|
* 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.server.service.security; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import org.thingsboard.server.actors.plugin.ValidationResult; |
||||
|
import org.thingsboard.server.actors.plugin.ValidationResultCode; |
||||
|
import org.thingsboard.server.extensions.api.exception.AccessDeniedException; |
||||
|
import org.thingsboard.server.extensions.api.exception.EntityNotFoundException; |
||||
|
import org.thingsboard.server.extensions.api.exception.InternalErrorException; |
||||
|
import org.thingsboard.server.extensions.api.exception.UnauthorizedException; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 31.03.18. |
||||
|
*/ |
||||
|
public class ValidationCallback<T> implements FutureCallback<ValidationResult> { |
||||
|
|
||||
|
private final T response; |
||||
|
private final FutureCallback<T> action; |
||||
|
|
||||
|
public ValidationCallback(T response, FutureCallback<T> action) { |
||||
|
this.response = response; |
||||
|
this.action = action; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onSuccess(ValidationResult result) { |
||||
|
ValidationResultCode resultCode = result.getResultCode(); |
||||
|
if (resultCode == ValidationResultCode.OK) { |
||||
|
action.onSuccess(response); |
||||
|
} else { |
||||
|
Exception e; |
||||
|
switch (resultCode) { |
||||
|
case ENTITY_NOT_FOUND: |
||||
|
e = new EntityNotFoundException(result.getMessage()); |
||||
|
break; |
||||
|
case UNAUTHORIZED: |
||||
|
e = new UnauthorizedException(result.getMessage()); |
||||
|
break; |
||||
|
case ACCESS_DENIED: |
||||
|
e = new AccessDeniedException(result.getMessage()); |
||||
|
break; |
||||
|
case INTERNAL_ERROR: |
||||
|
e = new InternalErrorException(result.getMessage()); |
||||
|
break; |
||||
|
default: |
||||
|
e = new UnauthorizedException("Permission denied."); |
||||
|
break; |
||||
|
} |
||||
|
onFailure(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
action.onFailure(e); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,335 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.msg.cluster.ServerAddress; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate; |
||||
|
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; |
||||
|
|
||||
|
import javax.annotation.Nullable; |
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.function.Function; |
||||
|
import java.util.function.Predicate; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
@Service |
||||
|
@Slf4j |
||||
|
public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService { |
||||
|
|
||||
|
@Autowired |
||||
|
private TelemetryWebSocketService wsService; |
||||
|
|
||||
|
@Autowired |
||||
|
private AttributesService attrService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TimeseriesService tsService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ClusterRoutingService routingService; |
||||
|
|
||||
|
private ExecutorService tsCallBackExecutor; |
||||
|
private ExecutorService wsCallBackExecutor; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
tsCallBackExecutor = Executors.newSingleThreadExecutor(); |
||||
|
wsCallBackExecutor = Executors.newSingleThreadExecutor(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (tsCallBackExecutor != null) { |
||||
|
tsCallBackExecutor.shutdownNow(); |
||||
|
} |
||||
|
if (wsCallBackExecutor != null) { |
||||
|
wsCallBackExecutor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>(); |
||||
|
|
||||
|
private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>(); |
||||
|
|
||||
|
@Override |
||||
|
public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) { |
||||
|
Optional<ServerAddress> server = routingService.resolveById(entityId); |
||||
|
Subscription subscription; |
||||
|
if (server.isPresent()) { |
||||
|
ServerAddress address = server.get(); |
||||
|
log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address); |
||||
|
subscription = new Subscription(sub, true, address); |
||||
|
// rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
|
||||
|
} else { |
||||
|
log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId); |
||||
|
subscription = new Subscription(sub, true); |
||||
|
} |
||||
|
registerSubscription(sessionId, entityId, subscription); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) { |
||||
|
cleanupLocalWsSessionSubscriptions(sessionId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void removeSubscription(String sessionId, int subscriptionId) { |
||||
|
log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); |
||||
|
Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId); |
||||
|
if (sessionSubscriptions != null) { |
||||
|
Subscription subscription = sessionSubscriptions.remove(subscriptionId); |
||||
|
if (subscription != null) { |
||||
|
processSubscriptionRemoval(sessionId, sessionSubscriptions, subscription); |
||||
|
} else { |
||||
|
log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] No session subscriptions found!", sessionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) { |
||||
|
saveAndNotify(entityId, ts, 0L, callback); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) { |
||||
|
ListenableFuture<List<Void>> saveFuture = tsService.save(entityId, ts, ttl); |
||||
|
addMainCallback(saveFuture, callback); |
||||
|
addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback) { |
||||
|
ListenableFuture<List<Void>> saveFuture = attrService.save(entityId, scope, attributes); |
||||
|
addMainCallback(saveFuture, callback); |
||||
|
addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes)); |
||||
|
} |
||||
|
|
||||
|
private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) { |
||||
|
Optional<ServerAddress> serverAddress = routingService.resolveById(entityId); |
||||
|
if (!serverAddress.isPresent()) { |
||||
|
onLocalAttributesUpdate(entityId, scope, attributes); |
||||
|
} else { |
||||
|
// rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, entries);
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void onTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) { |
||||
|
Optional<ServerAddress> serverAddress = routingService.resolveById(entityId); |
||||
|
if (!serverAddress.isPresent()) { |
||||
|
onLocalTimeseriesUpdate(entityId, ts); |
||||
|
} else { |
||||
|
// rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries);
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void onLocalAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) { |
||||
|
onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> { |
||||
|
List<TsKvEntry> subscriptionUpdate = null; |
||||
|
for (AttributeKvEntry kv : attributes) { |
||||
|
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) { |
||||
|
if (subscriptionUpdate == null) { |
||||
|
subscriptionUpdate = new ArrayList<>(); |
||||
|
} |
||||
|
subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv)); |
||||
|
} |
||||
|
} |
||||
|
return subscriptionUpdate; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void onLocalTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) { |
||||
|
onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> { |
||||
|
List<TsKvEntry> subscriptionUpdate = null; |
||||
|
for (TsKvEntry kv : ts) { |
||||
|
if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) { |
||||
|
if (subscriptionUpdate == null) { |
||||
|
subscriptionUpdate = new ArrayList<>(); |
||||
|
} |
||||
|
subscriptionUpdate.add(kv); |
||||
|
} |
||||
|
} |
||||
|
return subscriptionUpdate; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) { |
||||
|
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId); |
||||
|
if (deviceSubscriptions != null) { |
||||
|
deviceSubscriptions.stream().filter(filter).forEach(s -> { |
||||
|
String sessionId = s.getWsSessionId(); |
||||
|
List<TsKvEntry> subscriptionUpdate = f.apply(s); |
||||
|
if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) { |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate); |
||||
|
if (s.isLocal()) { |
||||
|
updateSubscriptionState(sessionId, s, update); |
||||
|
wsService.sendWsMsg(sessionId, update); |
||||
|
} else { |
||||
|
//TODO: ashvayka
|
||||
|
// rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
log.debug("[{}] No device subscriptions to process!", entityId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) { |
||||
|
log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update); |
||||
|
update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue())); |
||||
|
} |
||||
|
|
||||
|
private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) { |
||||
|
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>()); |
||||
|
deviceSubscriptions.add(subscription); |
||||
|
Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.computeIfAbsent(sessionId, k -> new HashMap<>()); |
||||
|
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); |
||||
|
} |
||||
|
|
||||
|
public void cleanupLocalWsSessionSubscriptions(String sessionId) { |
||||
|
cleanupWsSessionSubscriptions(sessionId, true); |
||||
|
} |
||||
|
|
||||
|
public void cleanupRemoteWsSessionSubscriptions(String sessionId) { |
||||
|
cleanupWsSessionSubscriptions(sessionId, false); |
||||
|
} |
||||
|
|
||||
|
private void cleanupWsSessionSubscriptions(String sessionId, boolean localSession) { |
||||
|
log.debug("[{}] Removing all subscriptions for particular session.", sessionId); |
||||
|
Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId); |
||||
|
if (sessionSubscriptions != null) { |
||||
|
int sessionSubscriptionSize = sessionSubscriptions.size(); |
||||
|
|
||||
|
for (Subscription subscription : sessionSubscriptions.values()) { |
||||
|
EntityId entityId = subscription.getEntityId(); |
||||
|
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId); |
||||
|
deviceSubscriptions.remove(subscription); |
||||
|
if (deviceSubscriptions.isEmpty()) { |
||||
|
subscriptionsByEntityId.remove(entityId); |
||||
|
} |
||||
|
} |
||||
|
subscriptionsByWsSessionId.remove(sessionId); |
||||
|
log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize); |
||||
|
|
||||
|
if (localSession) { |
||||
|
notifyWsSubscriptionClosed(sessionId, sessionSubscriptions); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] No subscriptions found!", sessionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void notifyWsSubscriptionClosed(String sessionId, Map<Integer, Subscription> sessionSubscriptions) { |
||||
|
Set<ServerAddress> affectedServers = new HashSet<>(); |
||||
|
for (Subscription subscription : sessionSubscriptions.values()) { |
||||
|
if (subscription.getServer() != null) { |
||||
|
affectedServers.add(subscription.getServer()); |
||||
|
} |
||||
|
} |
||||
|
for (ServerAddress address : affectedServers) { |
||||
|
log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address); |
||||
|
// rpcHandler.onSessionClose(ctx, address, sessionId);
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) { |
||||
|
EntityId entityId = subscription.getEntityId(); |
||||
|
if (subscription.isLocal() && subscription.getServer() != null) { |
||||
|
// rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId());
|
||||
|
} |
||||
|
if (sessionSubscriptions.isEmpty()) { |
||||
|
log.debug("[{}] Removed last subscription for particular session.", sessionId); |
||||
|
subscriptionsByWsSessionId.remove(sessionId); |
||||
|
} else { |
||||
|
log.debug("[{}] Removed session subscription.", sessionId); |
||||
|
} |
||||
|
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId); |
||||
|
if (deviceSubscriptions != null) { |
||||
|
boolean result = deviceSubscriptions.remove(subscription); |
||||
|
if (result) { |
||||
|
if (deviceSubscriptions.size() == 0) { |
||||
|
log.debug("[{}] Removed last subscription for particular device.", sessionId); |
||||
|
subscriptionsByEntityId.remove(entityId); |
||||
|
} else { |
||||
|
log.debug("[{}] Removed device subscription.", sessionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] Subscription not found!", sessionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] No device subscriptions found!", sessionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addMainCallback(ListenableFuture<List<Void>> saveFuture, final FutureCallback<Void> callback) { |
||||
|
Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable List<Void> result) { |
||||
|
callback.onSuccess(null); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}, tsCallBackExecutor); |
||||
|
} |
||||
|
|
||||
|
private void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) { |
||||
|
Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable List<Void> result) { |
||||
|
callback.accept(null); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
} |
||||
|
}, wsCallBackExecutor); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,563 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.google.common.base.Function; |
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.hazelcast.util.function.Consumer; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.server.actors.plugin.ValidationResult; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.kv.Aggregation; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseTsKvQuery; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvQuery; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.extensions.api.exception.UnauthorizedException; |
||||
|
import org.thingsboard.server.extensions.api.plugins.PluginContext; |
||||
|
import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.AttributesSubscriptionCmd; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.GetHistoryCmd; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TimeseriesSubscriptionCmd; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate; |
||||
|
import org.thingsboard.server.service.security.AccessValidator; |
||||
|
|
||||
|
import javax.annotation.Nullable; |
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ConcurrentMap; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
@Service |
||||
|
@Slf4j |
||||
|
public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService { |
||||
|
|
||||
|
public static final int DEFAULT_LIMIT = 100; |
||||
|
public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE; |
||||
|
private static final int UNKNOWN_SUBSCRIPTION_ID = 0; |
||||
|
private static final String PROCESSING_MSG = "[{}] Processing: {}"; |
||||
|
private static final ObjectMapper jsonMapper = new ObjectMapper(); |
||||
|
private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; |
||||
|
private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; |
||||
|
private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; |
||||
|
|
||||
|
private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Autowired |
||||
|
private TelemetrySubscriptionService subscriptionManager; |
||||
|
|
||||
|
@Autowired |
||||
|
private TelemetryWebSocketMsgEndpoint msgEndpoint; |
||||
|
|
||||
|
@Autowired |
||||
|
private AccessValidator accessValidator; |
||||
|
|
||||
|
@Autowired |
||||
|
private AttributesService attributesService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TimeseriesService tsService; |
||||
|
|
||||
|
private ExecutorService executor; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
executor = Executors.newSingleThreadExecutor(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (executor != null) { |
||||
|
executor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { |
||||
|
String sessionId = sessionRef.getSessionId(); |
||||
|
log.debug(PROCESSING_MSG, sessionId, event); |
||||
|
switch (event.getEventType()) { |
||||
|
case ESTABLISHED: |
||||
|
wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); |
||||
|
break; |
||||
|
case ERROR: |
||||
|
log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); |
||||
|
break; |
||||
|
case CLOSED: |
||||
|
wsSessionsMap.remove(sessionId); |
||||
|
subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) { |
||||
|
if (log.isTraceEnabled()) { |
||||
|
log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); |
||||
|
if (cmdsWrapper != null) { |
||||
|
if (cmdsWrapper.getAttrSubCmds() != null) { |
||||
|
cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd)); |
||||
|
} |
||||
|
if (cmdsWrapper.getTsSubCmds() != null) { |
||||
|
cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd)); |
||||
|
} |
||||
|
if (cmdsWrapper.getHistoryCmds() != null) { |
||||
|
cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); |
||||
|
} |
||||
|
} |
||||
|
} catch (IOException e) { |
||||
|
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void sendWsMsg(String sessionId, SubscriptionUpdate update) { |
||||
|
WsSessionMetaData md = wsSessionsMap.get(sessionId); |
||||
|
if (md != null) { |
||||
|
sendWsMsg(md.getSessionRef(), update); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) { |
||||
|
String sessionId = sessionRef.getSessionId(); |
||||
|
log.debug("[{}] Processing: {}", sessionId, cmd); |
||||
|
|
||||
|
if (validateSessionMetadata(sessionRef, cmd, sessionId)) { |
||||
|
if (cmd.isUnsubscribe()) { |
||||
|
unsubscribe(sessionRef, cmd, sessionId); |
||||
|
} else if (validateSubscriptionCmd(sessionRef, cmd)) { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); |
||||
|
log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); |
||||
|
Optional<Set<String>> keysOptional = getKeys(cmd); |
||||
|
if (keysOptional.isPresent()) { |
||||
|
List<String> keys = new ArrayList<>(keysOptional.get()); |
||||
|
handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys); |
||||
|
} else { |
||||
|
handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, |
||||
|
AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId, |
||||
|
List<String> keys) { |
||||
|
FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<AttributeKvEntry> data) { |
||||
|
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); |
||||
|
sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); |
||||
|
|
||||
|
Map<String, Long> subState = new HashMap<>(keys.size()); |
||||
|
keys.forEach(key -> subState.put(key, 0L)); |
||||
|
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); |
||||
|
|
||||
|
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope()); |
||||
|
subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error(FAILED_TO_FETCH_ATTRIBUTES, e); |
||||
|
SubscriptionUpdate update; |
||||
|
if (UnauthorizedException.class.isInstance(e)) { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, |
||||
|
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); |
||||
|
} else { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
FAILED_TO_FETCH_ATTRIBUTES); |
||||
|
} |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
if (StringUtils.isEmpty(cmd.getScope())) { |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, keys, callback)); |
||||
|
} else { |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), keys, callback)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) { |
||||
|
String sessionId = sessionRef.getSessionId(); |
||||
|
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); |
||||
|
if (sessionMD == null) { |
||||
|
log.warn("[{}] Session meta data not found. ", sessionId); |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
SESSION_META_DATA_NOT_FOUND); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
return; |
||||
|
} |
||||
|
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, |
||||
|
"Device id is empty!"); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
return; |
||||
|
} |
||||
|
if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) { |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, |
||||
|
"Keys are empty!"); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
return; |
||||
|
} |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); |
||||
|
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
||||
|
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) |
||||
|
.collect(Collectors.toList()); |
||||
|
|
||||
|
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<TsKvEntry> data) { |
||||
|
sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
SubscriptionUpdate update; |
||||
|
if (UnauthorizedException.class.isInstance(e)) { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, |
||||
|
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); |
||||
|
} else { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
FAILED_TO_FETCH_DATA); |
||||
|
} |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
}; |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, |
||||
|
on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure)); |
||||
|
} |
||||
|
|
||||
|
private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef, |
||||
|
AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) { |
||||
|
FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<AttributeKvEntry> data) { |
||||
|
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); |
||||
|
sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); |
||||
|
|
||||
|
Map<String, Long> subState = new HashMap<>(attributesData.size()); |
||||
|
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); |
||||
|
|
||||
|
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope()); |
||||
|
subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error(FAILED_TO_FETCH_ATTRIBUTES, e); |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
FAILED_TO_FETCH_ATTRIBUTES); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
if (StringUtils.isEmpty(cmd.getScope())) { |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, callback)); |
||||
|
} else { |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), callback)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) { |
||||
|
String sessionId = sessionRef.getSessionId(); |
||||
|
log.debug("[{}] Processing: {}", sessionId, cmd); |
||||
|
|
||||
|
if (validateSessionMetadata(sessionRef, cmd, sessionId)) { |
||||
|
if (cmd.isUnsubscribe()) { |
||||
|
unsubscribe(sessionRef, cmd, sessionId); |
||||
|
} else if (validateSubscriptionCmd(sessionRef, cmd)) { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); |
||||
|
Optional<Set<String>> keysOptional = getKeys(cmd); |
||||
|
|
||||
|
if (keysOptional.isPresent()) { |
||||
|
handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId); |
||||
|
} else { |
||||
|
handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, |
||||
|
TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { |
||||
|
long startTs; |
||||
|
if (cmd.getTimeWindow() > 0) { |
||||
|
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
||||
|
log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId); |
||||
|
startTs = cmd.getStartTs(); |
||||
|
long endTs = cmd.getStartTs() + cmd.getTimeWindow(); |
||||
|
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), |
||||
|
getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); |
||||
|
|
||||
|
final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, |
||||
|
on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure)); |
||||
|
} else { |
||||
|
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
||||
|
startTs = System.currentTimeMillis(); |
||||
|
log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId); |
||||
|
final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys); |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, |
||||
|
on(r -> Futures.addCallback(tsService.findLatest(entityId, keys), callback, executor), callback::onFailure)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef, |
||||
|
TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) { |
||||
|
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<TsKvEntry> data) { |
||||
|
sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); |
||||
|
Map<String, Long> subState = new HashMap<>(data.size()); |
||||
|
data.forEach(v -> subState.put(v.getKey(), v.getTs())); |
||||
|
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope()); |
||||
|
subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
SubscriptionUpdate update; |
||||
|
if (UnauthorizedException.class.isInstance(e)) { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, |
||||
|
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); |
||||
|
} else { |
||||
|
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
FAILED_TO_FETCH_DATA); |
||||
|
} |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
}; |
||||
|
accessValidator.validate(sessionRef.getSecurityCtx(), entityId, |
||||
|
on(r -> Futures.addCallback(tsService.findAllLatest(entityId), callback, executor), callback::onFailure)); |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) { |
||||
|
return new FutureCallback<List<TsKvEntry>>() { |
||||
|
@Override |
||||
|
public void onSuccess(List<TsKvEntry> data) { |
||||
|
sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); |
||||
|
|
||||
|
Map<String, Long> subState = new HashMap<>(keys.size()); |
||||
|
keys.forEach(key -> subState.put(key, startTs)); |
||||
|
data.forEach(v -> subState.put(v.getKey(), v.getTs())); |
||||
|
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope()); |
||||
|
subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable e) { |
||||
|
log.error(FAILED_TO_FETCH_DATA, e); |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
FAILED_TO_FETCH_DATA); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { |
||||
|
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
||||
|
subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId); |
||||
|
} else { |
||||
|
subscriptionManager.removeSubscription(sessionId, cmd.getCmdId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { |
||||
|
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, |
||||
|
"Device id is empty!"); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { |
||||
|
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); |
||||
|
if (sessionMD == null) { |
||||
|
log.warn("[{}] Session meta data not found. ", sessionId); |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, |
||||
|
SESSION_META_DATA_NOT_FOUND); |
||||
|
sendWsMsg(sessionRef, update); |
||||
|
return false; |
||||
|
} else { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { |
||||
|
try { |
||||
|
msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update)); |
||||
|
} catch (JsonProcessingException e) { |
||||
|
log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); |
||||
|
} catch (IOException e) { |
||||
|
log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { |
||||
|
if (!StringUtils.isEmpty(cmd.getKeys())) { |
||||
|
Set<String> keys = new HashSet<>(); |
||||
|
Collections.addAll(keys, cmd.getKeys().split(",")); |
||||
|
return Optional.of(keys); |
||||
|
} else { |
||||
|
return Optional.empty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) { |
||||
|
return Futures.transform(Futures.successfulAsList(futures), |
||||
|
(Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> { |
||||
|
List<AttributeKvEntry> tmp = new ArrayList<>(); |
||||
|
if (input != null) { |
||||
|
input.forEach(tmp::addAll); |
||||
|
} |
||||
|
return tmp; |
||||
|
}, executor); |
||||
|
} |
||||
|
|
||||
|
private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) { |
||||
|
return new FutureCallback<ValidationResult>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable ValidationResult result) { |
||||
|
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>(); |
||||
|
for (String scope : DataConstants.allScopes()) { |
||||
|
futures.add(attributesService.find(entityId, scope, keys)); |
||||
|
} |
||||
|
|
||||
|
ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures); |
||||
|
Futures.addCallback(future, callback); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) { |
||||
|
return new FutureCallback<ValidationResult>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable ValidationResult result) { |
||||
|
Futures.addCallback(attributesService.find(entityId, scope, keys), callback); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final FutureCallback<List<AttributeKvEntry>> callback) { |
||||
|
return new FutureCallback<ValidationResult>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable ValidationResult result) { |
||||
|
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>(); |
||||
|
for (String scope : DataConstants.allScopes()) { |
||||
|
futures.add(attributesService.findAll(entityId, scope)); |
||||
|
} |
||||
|
|
||||
|
ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures); |
||||
|
Futures.addCallback(future, callback); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback<List<AttributeKvEntry>> callback) { |
||||
|
return new FutureCallback<ValidationResult>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable ValidationResult result) { |
||||
|
Futures.addCallback(attributesService.findAll(entityId, scope), callback); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
callback.onFailure(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private FutureCallback<ValidationResult> on(Consumer<ValidationResult> success, Consumer<Throwable> failure) { |
||||
|
return new FutureCallback<ValidationResult>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable ValidationResult result) { |
||||
|
success.accept(result); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
failure.accept(t); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static Aggregation getAggregation(String agg) { |
||||
|
return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg); |
||||
|
} |
||||
|
|
||||
|
private int getLimit(int limit) { |
||||
|
return limit == 0 ? DEFAULT_LIMIT : limit; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public interface TelemetrySubscriptionService extends RuleEngineTelemetryService { |
||||
|
|
||||
|
void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub); |
||||
|
|
||||
|
void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId); |
||||
|
|
||||
|
void removeSubscription(String sessionId, int cmdId); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public interface TelemetryWebSocketMsgEndpoint { |
||||
|
|
||||
|
void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException; |
||||
|
|
||||
|
void close(TelemetryWebSocketSessionRef sessionRef) throws IOException; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; |
||||
|
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public interface TelemetryWebSocketService { |
||||
|
|
||||
|
void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent); |
||||
|
|
||||
|
void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); |
||||
|
|
||||
|
void sendWsMsg(String sessionId, SubscriptionUpdate update); |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
import java.net.InetSocketAddress; |
||||
|
import java.util.Objects; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public class TelemetryWebSocketSessionRef { |
||||
|
|
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
@Getter |
||||
|
private final String sessionId; |
||||
|
@Getter |
||||
|
private final SecurityUser securityCtx; |
||||
|
@Getter |
||||
|
private final InetSocketAddress localAddress; |
||||
|
@Getter |
||||
|
private final InetSocketAddress remoteAddress; |
||||
|
|
||||
|
public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) { |
||||
|
this.sessionId = sessionId; |
||||
|
this.securityCtx = securityCtx; |
||||
|
this.localAddress = localAddress; |
||||
|
this.remoteAddress = remoteAddress; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
if (this == o) return true; |
||||
|
if (o == null || getClass() != o.getClass()) return false; |
||||
|
TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o; |
||||
|
return Objects.equals(sessionId, that.sessionId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return Objects.hash(sessionId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "TelemetryWebSocketSessionRef{" + |
||||
|
"sessionId='" + sessionId + '\'' + |
||||
|
", localAddress=" + localAddress + |
||||
|
", remoteAddress=" + remoteAddress + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
import java.net.InetSocketAddress; |
||||
|
import java.util.Objects; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
public class TelemetryWebSocketTextMsg { |
||||
|
|
||||
|
private final TelemetryWebSocketSessionRef sessionRef; |
||||
|
private final String payload; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
/** |
||||
|
* 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.server.service.telemetry; |
||||
|
|
||||
|
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public class WsSessionMetaData { |
||||
|
private TelemetryWebSocketSessionRef sessionRef; |
||||
|
private long lastActivityTime; |
||||
|
|
||||
|
public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) { |
||||
|
super(); |
||||
|
this.sessionRef = sessionRef; |
||||
|
this.lastActivityTime = System.currentTimeMillis(); |
||||
|
} |
||||
|
|
||||
|
public TelemetryWebSocketSessionRef getSessionRef() { |
||||
|
return sessionRef; |
||||
|
} |
||||
|
|
||||
|
public void setSessionRef(TelemetryWebSocketSessionRef sessionRef) { |
||||
|
this.sessionRef = sessionRef; |
||||
|
} |
||||
|
|
||||
|
public long getLastActivityTime() { |
||||
|
return lastActivityTime; |
||||
|
} |
||||
|
|
||||
|
public void setLastActivityTime(long lastActivityTime) { |
||||
|
this.lastActivityTime = lastActivityTime; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]"; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue