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