Merge branch 'master' into develop/2.3
This commit is contained in:
		
						commit
						4893200a1e
					
				@ -264,6 +264,10 @@
 | 
			
		||||
            <groupId>org.javadelight</groupId>
 | 
			
		||||
            <artifactId>delight-nashorn-sandbox</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.springfox.ui</groupId>
 | 
			
		||||
            <artifactId>springfox-swagger-ui-rfc6570</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,6 @@
 | 
			
		||||
-- limitations under the License.
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name;
 | 
			
		||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text;
 | 
			
		||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer;
 | 
			
		||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_entity_id;
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS thingsboard.entity_views;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS thingsboard.entity_views (
 | 
			
		||||
    id timeuuid,
 | 
			
		||||
    entity_id timeuuid,
 | 
			
		||||
 | 
			
		||||
@ -14,10 +14,8 @@
 | 
			
		||||
-- limitations under the License.
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS entity_views;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS entity_views (
 | 
			
		||||
    id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
 | 
			
		||||
    id varchar(31) NOT NULL CONSTRAINT entity_views_pkey PRIMARY KEY,
 | 
			
		||||
    entity_id varchar(31),
 | 
			
		||||
    entity_type varchar(255),
 | 
			
		||||
    tenant_id varchar(31),
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
package org.thingsboard.server.actors.device;
 | 
			
		||||
 | 
			
		||||
import akka.actor.ActorContext;
 | 
			
		||||
import akka.event.LoggingAdapter;
 | 
			
		||||
import com.datastax.driver.core.utils.UUIDs;
 | 
			
		||||
import com.google.common.util.concurrent.FutureCallback;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
@ -26,12 +25,12 @@ import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParser;
 | 
			
		||||
import com.google.protobuf.InvalidProtocolBufferException;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.collections.CollectionUtils;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RpcError;
 | 
			
		||||
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
 | 
			
		||||
import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
 | 
			
		||||
import org.thingsboard.server.actors.ActorSystemContext;
 | 
			
		||||
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
 | 
			
		||||
import org.thingsboard.server.common.data.DataConstants;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
@ -42,7 +41,6 @@ import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsgDataType;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
 | 
			
		||||
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
 | 
			
		||||
import org.thingsboard.server.common.msg.session.SessionMsgType;
 | 
			
		||||
import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
 | 
			
		||||
@ -81,12 +79,14 @@ import java.util.HashSet;
 | 
			
		||||
import java.util.LinkedHashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
 | 
			
		||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Andrew Shvayka
 | 
			
		||||
 */
 | 
			
		||||
@ -263,10 +263,8 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleGetAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
 | 
			
		||||
        ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, toOptionalSet(request.getClientAttributeNamesList()));
 | 
			
		||||
        ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, toOptionalSet(request.getSharedAttributeNamesList()));
 | 
			
		||||
        int requestId = request.getRequestId();
 | 
			
		||||
        Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
 | 
			
		||||
        Futures.addCallback(getAttributesKvEntries(request), new FutureCallback<List<List<AttributeKvEntry>>>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onSuccess(@Nullable List<List<AttributeKvEntry>> result) {
 | 
			
		||||
                GetAttributeResponseMsg responseMsg = GetAttributeResponseMsg.newBuilder()
 | 
			
		||||
@ -287,16 +285,35 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<List<AttributeKvEntry>> getAttributeKvEntries(DeviceId deviceId, String scope, Optional<Set<String>> names) {
 | 
			
		||||
        if (names.isPresent()) {
 | 
			
		||||
            if (!names.get().isEmpty()) {
 | 
			
		||||
                return systemContext.getAttributesService().find(tenantId, deviceId, scope, names.get());
 | 
			
		||||
    private ListenableFuture<List<List<AttributeKvEntry>>> getAttributesKvEntries(GetAttributeRequestMsg request) {
 | 
			
		||||
        ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture;
 | 
			
		||||
        ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture;
 | 
			
		||||
        if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
 | 
			
		||||
            clientAttributesFuture = findAllAttributesByScope(CLIENT_SCOPE);
 | 
			
		||||
            sharedAttributesFuture = findAllAttributesByScope(SHARED_SCOPE);
 | 
			
		||||
        } else if (!CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
 | 
			
		||||
            clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
 | 
			
		||||
            sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
 | 
			
		||||
        } else if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
 | 
			
		||||
            clientAttributesFuture = Futures.immediateFuture(Collections.emptyList());
 | 
			
		||||
            sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
 | 
			
		||||
        } else {
 | 
			
		||||
            sharedAttributesFuture = Futures.immediateFuture(Collections.emptyList());
 | 
			
		||||
            clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
 | 
			
		||||
        }
 | 
			
		||||
        return Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<List<AttributeKvEntry>> findAllAttributesByScope(String scope) {
 | 
			
		||||
        return systemContext.getAttributesService().findAll(tenantId, deviceId, scope);
 | 
			
		||||
    }
 | 
			
		||||
        } else {
 | 
			
		||||
            return Futures.immediateFuture(Collections.emptyList());
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<List<AttributeKvEntry>> findAttributesByScope(Set<String> attributesSet, String scope) {
 | 
			
		||||
        return systemContext.getAttributesService().find(tenantId, deviceId, scope, attributesSet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Set<String> toSet(List<String> strings) {
 | 
			
		||||
        return new HashSet<>(strings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handlePostAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, PostAttributeMsg postAttributes) {
 | 
			
		||||
@ -368,7 +385,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 | 
			
		||||
            AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
 | 
			
		||||
            if (msg.isDeleted()) {
 | 
			
		||||
                List<String> sharedKeys = msg.getDeletedKeys().stream()
 | 
			
		||||
                        .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
 | 
			
		||||
                        .filter(key -> SHARED_SCOPE.equals(key.getScope()))
 | 
			
		||||
                        .map(AttributeKey::getAttributeKey)
 | 
			
		||||
                        .collect(Collectors.toList());
 | 
			
		||||
                if (!sharedKeys.isEmpty()) {
 | 
			
		||||
@ -376,7 +393,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 | 
			
		||||
                    hasNotificationData = true;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
 | 
			
		||||
                if (SHARED_SCOPE.equals(msg.getScope())) {
 | 
			
		||||
                    List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
 | 
			
		||||
                    if (attributes.size() > 0) {
 | 
			
		||||
                        List<TsKvProto> sharedUpdated = msg.getValues().stream().map(this::toTsKvProto)
 | 
			
		||||
@ -545,14 +562,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 | 
			
		||||
        return json;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<Set<String>> toOptionalSet(List<String> strings) {
 | 
			
		||||
        if (strings == null || strings.isEmpty()) {
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        } else {
 | 
			
		||||
            return Optional.of(new HashSet<>(strings));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendToTransport(GetAttributeResponseMsg responseMsg, SessionInfoProto sessionInfo) {
 | 
			
		||||
        DeviceActorToTransportMsg msg = DeviceActorToTransportMsg.newBuilder()
 | 
			
		||||
                .setSessionIdMSB(sessionInfo.getSessionIdMSB())
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ 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;
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,6 @@
 | 
			
		||||
package org.thingsboard.server.actors.service;
 | 
			
		||||
 | 
			
		||||
import akka.actor.ActorRef;
 | 
			
		||||
import akka.event.Logging;
 | 
			
		||||
import akka.event.LoggingAdapter;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.server.actors.ActorSystemContext;
 | 
			
		||||
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
 | 
			
		||||
import org.thingsboard.server.actors.stats.StatsPersistMsg;
 | 
			
		||||
@ -60,7 +57,8 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
 | 
			
		||||
                scheduleStatsPersistTick();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.warn("[{}][{}] Failed to start {} processor: {}", tenantId, id, id.getEntityType(), e);
 | 
			
		||||
            log.warn("[{}][{}] Failed to start {} processor.", tenantId, id, id.getEntityType());
 | 
			
		||||
            log.warn("Error:", e);
 | 
			
		||||
            logAndPersist("OnStart", e, true);
 | 
			
		||||
            logLifecycleEvent(ComponentLifecycleEvent.STARTED, e);
 | 
			
		||||
        }
 | 
			
		||||
@ -149,10 +147,13 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
 | 
			
		||||
 | 
			
		||||
    private void logAndPersist(String method, Exception e, boolean critical) {
 | 
			
		||||
        errorsOccurred++;
 | 
			
		||||
        String componentName = processor != null ? processor.getComponentName() : "Unknown";
 | 
			
		||||
        if (critical) {
 | 
			
		||||
            log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
 | 
			
		||||
            log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
 | 
			
		||||
            log.warn("Critical Error: ", e);
 | 
			
		||||
        } else {
 | 
			
		||||
            log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
 | 
			
		||||
            log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
 | 
			
		||||
            log.debug("Debug Error: ", e);
 | 
			
		||||
        }
 | 
			
		||||
        long ts = System.currentTimeMillis();
 | 
			
		||||
        if (ts - lastPersistedErrorTs > getErrorPersistFrequency()) {
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,8 @@ public class SwaggerConfiguration {
 | 
			
		||||
                    .paths(apiPaths())
 | 
			
		||||
                    .build()
 | 
			
		||||
                    .securitySchemes(newArrayList(jwtTokenKey()))
 | 
			
		||||
                    .securityContexts(newArrayList(securityContext()));
 | 
			
		||||
                    .securityContexts(newArrayList(securityContext()))
 | 
			
		||||
                    .enableUrlTemplating(true);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      private ApiKey jwtTokenKey() {
 | 
			
		||||
 | 
			
		||||
@ -280,9 +280,15 @@ public class TelemetryController extends BaseController {
 | 
			
		||||
            deleteFromTs = 0L;
 | 
			
		||||
            deleteToTs = System.currentTimeMillis();
 | 
			
		||||
        } else {
 | 
			
		||||
            if (startTs == null || endTs == null) {
 | 
			
		||||
                deleteToTs = endTs;
 | 
			
		||||
                return getImmediateDeferredResult("When deleteAllDataForKeys is false, start and end timestamp values shouldn't be empty", HttpStatus.BAD_REQUEST);
 | 
			
		||||
            }
 | 
			
		||||
            else{
 | 
			
		||||
                deleteFromTs = startTs;
 | 
			
		||||
                deleteToTs = endTs;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> {
 | 
			
		||||
            List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.util.StringUtils;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
@ -100,7 +101,7 @@ public class LocalTransportApiService implements TransportApiService {
 | 
			
		||||
        //TODO: Make async and enable caching
 | 
			
		||||
        DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId);
 | 
			
		||||
        if (credentials != null && credentials.getCredentialsType() == credentialsType) {
 | 
			
		||||
            return getDeviceInfo(credentials.getDeviceId());
 | 
			
		||||
            return getDeviceInfo(credentials.getDeviceId(), credentials);
 | 
			
		||||
        } else {
 | 
			
		||||
            return getEmptyTransportApiResponseFuture();
 | 
			
		||||
        }
 | 
			
		||||
@ -135,15 +136,20 @@ public class LocalTransportApiService implements TransportApiService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId) {
 | 
			
		||||
    private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
 | 
			
		||||
        return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, deviceId), device -> {
 | 
			
		||||
            if (device == null) {
 | 
			
		||||
                log.trace("[{}] Failed to lookup device by id", deviceId);
 | 
			
		||||
                return getEmptyTransportApiResponse();
 | 
			
		||||
            }
 | 
			
		||||
            try {
 | 
			
		||||
                ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
 | 
			
		||||
                builder.setDeviceInfo(getDeviceInfoProto(device));
 | 
			
		||||
                if(!StringUtils.isEmpty(credentials.getCredentialsValue())){
 | 
			
		||||
                    builder.setCredentialsBody(credentials.getCredentialsValue());
 | 
			
		||||
                }
 | 
			
		||||
                return TransportApiResponseMsg.newBuilder()
 | 
			
		||||
                        .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build();
 | 
			
		||||
                        .setValidateTokenResponseMsg(builder.build()).build();
 | 
			
		||||
            } catch (JsonProcessingException e) {
 | 
			
		||||
                log.warn("[{}] Failed to lookup device by id", deviceId, e);
 | 
			
		||||
                return getEmptyTransportApiResponse();
 | 
			
		||||
 | 
			
		||||
@ -54,18 +54,18 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Component("MqttSslHandlerProvider")
 | 
			
		||||
@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.http.enabled}'=='true')")
 | 
			
		||||
@ConditionalOnProperty(prefix = "mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
 | 
			
		||||
@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
 | 
			
		||||
public class MqttSslHandlerProvider {
 | 
			
		||||
 | 
			
		||||
    @Value("${mqtt.ssl.protocol}")
 | 
			
		||||
    @Value("${transport.mqtt.ssl.protocol}")
 | 
			
		||||
    private String sslProtocol;
 | 
			
		||||
    @Value("${mqtt.ssl.key_store}")
 | 
			
		||||
    @Value("${transport.mqtt.ssl.key_store}")
 | 
			
		||||
    private String keyStoreFile;
 | 
			
		||||
    @Value("${mqtt.ssl.key_store_password}")
 | 
			
		||||
    @Value("${transport.mqtt.ssl.key_store_password}")
 | 
			
		||||
    private String keyStorePassword;
 | 
			
		||||
    @Value("${mqtt.ssl.key_password}")
 | 
			
		||||
    @Value("${transport.mqtt.ssl.key_password}")
 | 
			
		||||
    private String keyPassword;
 | 
			
		||||
    @Value("${mqtt.ssl.key_store_type}")
 | 
			
		||||
    @Value("${transport.mqtt.ssl.key_store_type}")
 | 
			
		||||
    private String keyStoreType;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
 | 
			
		||||
@ -271,6 +271,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
 | 
			
		||||
                    case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC:
 | 
			
		||||
                    case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC:
 | 
			
		||||
                    case MqttTopics.GATEWAY_RPC_TOPIC:
 | 
			
		||||
                    case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC:
 | 
			
		||||
                    case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC:
 | 
			
		||||
                        registerSubQoS(topic, grantedQoSList, reqQoS);
 | 
			
		||||
                        break;
 | 
			
		||||
 | 
			
		||||
@ -36,9 +36,8 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
 | 
			
		||||
    @Override
 | 
			
		||||
    public void initChannel(SocketChannel ch) {
 | 
			
		||||
        ChannelPipeline pipeline = ch.pipeline();
 | 
			
		||||
        SslHandler sslHandler = null;
 | 
			
		||||
        if (context.getSslHandlerProvider() != null) {
 | 
			
		||||
            sslHandler = context.getSslHandlerProvider().getSslHandler();
 | 
			
		||||
            SslHandler sslHandler = context.getSslHandlerProvider().getSslHandler();
 | 
			
		||||
            pipeline.addLast(sslHandler);
 | 
			
		||||
            context.setSslHandler(sslHandler);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ public abstract class AbstractTransportService implements TransportService {
 | 
			
		||||
    private boolean rateLimitEnabled;
 | 
			
		||||
    @Value("${transport.rate_limits.tenant}")
 | 
			
		||||
    private String perTenantLimitsConf;
 | 
			
		||||
    @Value("${transport.rate_limits.tenant}")
 | 
			
		||||
    @Value("${transport.rate_limits.device}")
 | 
			
		||||
    private String perDevicesLimitsConf;
 | 
			
		||||
    @Value("${transport.sessions.inactivity_timeout}")
 | 
			
		||||
    private long sessionInactivityTimeout;
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.dao.util;
 | 
			
		||||
 | 
			
		||||
import com.datastax.driver.core.*;
 | 
			
		||||
import com.google.common.util.concurrent.FutureCallback;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
@ -22,6 +23,7 @@ import com.google.common.util.concurrent.SettableFuture;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.msg.tools.TbRateLimits;
 | 
			
		||||
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
@ -183,12 +185,39 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
 | 
			
		||||
 | 
			
		||||
    private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
 | 
			
		||||
        if (log.isTraceEnabled()) {
 | 
			
		||||
            if (taskCtx.getTask() instanceof CassandraStatementTask) {
 | 
			
		||||
                CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask();
 | 
			
		||||
                if (cassStmtTask.getStatement() instanceof BoundStatement) {
 | 
			
		||||
                    BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement();
 | 
			
		||||
                    String query = toStringWithValues(stmt, ProtocolVersion.V5);
 | 
			
		||||
                    log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            log.debug("[{}] {} task", taskCtx.getId(), action);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static String toStringWithValues(BoundStatement boundStatement, ProtocolVersion protocolVersion) {
 | 
			
		||||
        CodecRegistry codecRegistry = boundStatement.preparedStatement().getCodecRegistry();
 | 
			
		||||
        PreparedStatement preparedStatement = boundStatement.preparedStatement();
 | 
			
		||||
        String query = preparedStatement.getQueryString();
 | 
			
		||||
        ColumnDefinitions defs = preparedStatement.getVariables();
 | 
			
		||||
        int index = 0;
 | 
			
		||||
        for (ColumnDefinitions.Definition def : defs) {
 | 
			
		||||
            DataType type = def.getType();
 | 
			
		||||
            TypeCodec<Object> codec = codecRegistry.codecFor(type);
 | 
			
		||||
            if (boundStatement.getBytesUnsafe(index) != null) {
 | 
			
		||||
                Object value = codec.deserialize(boundStatement.getBytesUnsafe(index), protocolVersion);
 | 
			
		||||
                query = query.replaceFirst("\\?", codec.format(value));
 | 
			
		||||
            }
 | 
			
		||||
            index++;
 | 
			
		||||
        }
 | 
			
		||||
        return query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected int getQueueSize() {
 | 
			
		||||
        return queue.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv (
 | 
			
		||||
  long_v bigint,
 | 
			
		||||
  dbl_v double precision,
 | 
			
		||||
  last_update_ts bigint,
 | 
			
		||||
  CONSTRAINT attribute_kv_unq_key UNIQUE (entity_type, entity_id, attribute_type, attribute_key)
 | 
			
		||||
  CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_type, entity_id, attribute_type, attribute_key)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS component_descriptor (
 | 
			
		||||
@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS relation (
 | 
			
		||||
    relation_type_group varchar(255),
 | 
			
		||||
    relation_type varchar(255),
 | 
			
		||||
    additional_info varchar,
 | 
			
		||||
    CONSTRAINT relation_unq_key UNIQUE (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
 | 
			
		||||
    CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS tb_user (
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS ts_kv (
 | 
			
		||||
    str_v varchar(10000000),
 | 
			
		||||
    long_v bigint,
 | 
			
		||||
    dbl_v double precision,
 | 
			
		||||
    CONSTRAINT ts_kv_unq_key UNIQUE (entity_type, entity_id, key, ts)
 | 
			
		||||
    CONSTRAINT ts_kv_pkey PRIMARY KEY (entity_type, entity_id, key, ts)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS ts_kv_latest (
 | 
			
		||||
@ -35,5 +35,5 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
 | 
			
		||||
    str_v varchar(10000000),
 | 
			
		||||
    long_v bigint,
 | 
			
		||||
    dbl_v double precision,
 | 
			
		||||
    CONSTRAINT ts_kv_latest_unq_key UNIQUE (entity_type, entity_id, key)
 | 
			
		||||
    CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_type, entity_id, key)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,8 @@ import org.apache.http.impl.client.HttpClients;
 | 
			
		||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 | 
			
		||||
import org.apache.http.ssl.SSLContextBuilder;
 | 
			
		||||
import org.apache.http.ssl.SSLContexts;
 | 
			
		||||
import org.junit.*;
 | 
			
		||||
import org.junit.BeforeClass;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.rules.TestRule;
 | 
			
		||||
import org.junit.rules.TestWatcher;
 | 
			
		||||
import org.junit.runner.Description;
 | 
			
		||||
@ -43,7 +44,10 @@ import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.*;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.SSLContext;
 | 
			
		||||
import javax.net.ssl.SSLSession;
 | 
			
		||||
import javax.net.ssl.SSLSocket;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.security.cert.X509Certificate;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@ -54,6 +58,7 @@ import java.util.Random;
 | 
			
		||||
public abstract class AbstractContainerTest {
 | 
			
		||||
    protected static final String HTTPS_URL = "https://localhost";
 | 
			
		||||
    protected static final String WSS_URL = "wss://localhost";
 | 
			
		||||
    protected static String TB_TOKEN;
 | 
			
		||||
    protected static RestClient restClient;
 | 
			
		||||
    protected ObjectMapper mapper = new ObjectMapper();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.msa.connectivity;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.google.common.collect.Sets;
 | 
			
		||||
import org.junit.Assert;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
@ -25,6 +26,17 @@ import org.thingsboard.server.msa.AbstractContainerTest;
 | 
			
		||||
import org.thingsboard.server.msa.WsClient;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static org.junit.Assert.assertEquals;
 | 
			
		||||
import static org.junit.Assert.assertFalse;
 | 
			
		||||
import static org.junit.Assert.assertNotNull;
 | 
			
		||||
import static org.junit.Assert.assertTrue;
 | 
			
		||||
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
 | 
			
		||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
 | 
			
		||||
 | 
			
		||||
public class HttpClientTest extends AbstractContainerTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
@ -52,6 +64,58 @@ public class HttpClientTest extends AbstractContainerTest {
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
        restClient.deleteDevice(device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getAttributes() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        TB_TOKEN = restClient.getToken();
 | 
			
		||||
 | 
			
		||||
        Device device = createDevice("test");
 | 
			
		||||
        String accessToken = restClient.getCredentials(device.getId()).getCredentialsId();
 | 
			
		||||
        assertNotNull(accessToken);
 | 
			
		||||
 | 
			
		||||
        ResponseEntity deviceSharedAttributes = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()),
 | 
			
		||||
                        ResponseEntity.class,
 | 
			
		||||
                        accessToken);
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        ResponseEntity deviceClientsAttributes = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()),
 | 
			
		||||
                        ResponseEntity.class,
 | 
			
		||||
                        accessToken);
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        TimeUnit.SECONDS.sleep(3);
 | 
			
		||||
 | 
			
		||||
        Optional<JsonNode> allOptional = restClient.getAttributes(accessToken, null, null);
 | 
			
		||||
        assertTrue(allOptional.isPresent());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        JsonNode all = allOptional.get();
 | 
			
		||||
        assertEquals(2, all.size());
 | 
			
		||||
        assertEquals(mapper.readTree(createPayload().toString()), all.get("shared"));
 | 
			
		||||
        assertEquals(mapper.readTree(createPayload().toString()), all.get("client"));
 | 
			
		||||
 | 
			
		||||
        Optional<JsonNode> sharedOptional = restClient.getAttributes(accessToken, null, "stringKey");
 | 
			
		||||
        assertTrue(sharedOptional.isPresent());
 | 
			
		||||
 | 
			
		||||
        JsonNode shared = sharedOptional.get();
 | 
			
		||||
        assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
 | 
			
		||||
        assertFalse(shared.has("client"));
 | 
			
		||||
 | 
			
		||||
        Optional<JsonNode> clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null);
 | 
			
		||||
        assertTrue(clientOptional.isPresent());
 | 
			
		||||
 | 
			
		||||
        JsonNode client = clientOptional.get();
 | 
			
		||||
        assertFalse(client.has("shared"));
 | 
			
		||||
        assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey"));
 | 
			
		||||
        assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
 | 
			
		||||
 | 
			
		||||
        restClient.deleteDevice(device.getId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -222,6 +222,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
 | 
			
		||||
 | 
			
		||||
    private void handlePuback(MqttPubAckMessage message) {
 | 
			
		||||
        MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId());
 | 
			
		||||
        if (pendingPublish == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        pendingPublish.getFuture().setSuccess(null);
 | 
			
		||||
        pendingPublish.onPubackReceived();
 | 
			
		||||
        this.client.getPendingPublishes().remove(message.variableHeader().messageId());
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,6 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.mqtt;
 | 
			
		||||
 | 
			
		||||
import io.netty.channel.ChannelId;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Created by Valerii Sosliuk on 12/30/2017.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							@ -70,6 +70,7 @@
 | 
			
		||||
        <surfire.version>2.19.1</surfire.version>
 | 
			
		||||
        <jar-plugin.version>3.0.2</jar-plugin.version>
 | 
			
		||||
        <springfox-swagger.version>2.6.1</springfox-swagger.version>
 | 
			
		||||
        <springfox-swagger-ui-rfc6570.version>1.0.0</springfox-swagger-ui-rfc6570.version>
 | 
			
		||||
        <bouncycastle.version>1.56</bouncycastle.version>
 | 
			
		||||
        <winsw.version>2.0.1</winsw.version>
 | 
			
		||||
        <hsqldb.version>2.4.0</hsqldb.version>
 | 
			
		||||
@ -794,6 +795,11 @@
 | 
			
		||||
                <artifactId>bucket4j-core</artifactId>
 | 
			
		||||
                <version>${bucket4j.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>io.springfox.ui</groupId>
 | 
			
		||||
                <artifactId>springfox-swagger-ui-rfc6570</artifactId>
 | 
			
		||||
                <version>${springfox-swagger-ui-rfc6570.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
        </dependencies>
 | 
			
		||||
    </dependencyManagement>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,9 +17,6 @@ package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Created by igor on 6/1/18.
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public abstract class TbAbstractCustomerActionNodeConfiguration {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,222 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import com.google.common.cache.CacheBuilder;
 | 
			
		||||
import com.google.common.cache.CacheLoader;
 | 
			
		||||
import com.google.common.cache.LoadingCache;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
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.rule.engine.api.util.TbNodeUtils;
 | 
			
		||||
import org.thingsboard.rule.engine.util.EntityContainer;
 | 
			
		||||
import org.thingsboard.server.common.data.Customer;
 | 
			
		||||
import org.thingsboard.server.common.data.DashboardInfo;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityView;
 | 
			
		||||
import org.thingsboard.server.common.data.asset.Asset;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageData;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageLink;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
import org.thingsboard.server.dao.asset.AssetService;
 | 
			
		||||
import org.thingsboard.server.dao.customer.CustomerService;
 | 
			
		||||
import org.thingsboard.server.dao.dashboard.DashboardService;
 | 
			
		||||
import org.thingsboard.server.dao.device.DeviceService;
 | 
			
		||||
import org.thingsboard.server.dao.entityview.EntityViewService;
 | 
			
		||||
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
 | 
			
		||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
 | 
			
		||||
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationActionNodeConfiguration> implements TbNode {
 | 
			
		||||
 | 
			
		||||
    protected C config;
 | 
			
		||||
    protected EntityId fromId;
 | 
			
		||||
    protected EntityId toId;
 | 
			
		||||
 | 
			
		||||
    private LoadingCache<Entitykey, EntityContainer> entityIdCache;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        this.config = loadEntityNodeActionConfig(configuration);
 | 
			
		||||
        CacheBuilder cacheBuilder = CacheBuilder.newBuilder();
 | 
			
		||||
        if (this.config.getEntityCacheExpiration() > 0) {
 | 
			
		||||
            cacheBuilder.expireAfterWrite(this.config.getEntityCacheExpiration(), TimeUnit.SECONDS);
 | 
			
		||||
        }
 | 
			
		||||
        entityIdCache = cacheBuilder
 | 
			
		||||
                .build(new EntityCacheLoader(ctx, createEntityIfNotExists()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMsg(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        withCallback(processEntityRelationAction(ctx, msg),
 | 
			
		||||
                filterResult -> ctx.tellNext(msg, filterResult ? SUCCESS : FAILURE), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processEntityRelationAction(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract boolean createEntityIfNotExists();
 | 
			
		||||
 | 
			
		||||
    protected abstract ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer);
 | 
			
		||||
 | 
			
		||||
    protected abstract C loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException;
 | 
			
		||||
 | 
			
		||||
    protected ListenableFuture<EntityContainer> getEntity(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        String entityName = TbNodeUtils.processPattern(this.config.getEntityNamePattern(), msg.getMetaData());
 | 
			
		||||
        String type = null;
 | 
			
		||||
        if (this.config.getEntityTypePattern() != null) {
 | 
			
		||||
            type = TbNodeUtils.processPattern(this.config.getEntityTypePattern(), msg.getMetaData());
 | 
			
		||||
        }
 | 
			
		||||
        EntityType entityType = EntityType.valueOf(this.config.getEntityType());
 | 
			
		||||
        Entitykey key = new Entitykey(entityName, type, entityType);
 | 
			
		||||
        return ctx.getDbCallbackExecutor().executeAsync(() -> {
 | 
			
		||||
            EntityContainer entityContainer = entityIdCache.get(key);
 | 
			
		||||
            if (entityContainer.getEntityId() == null) {
 | 
			
		||||
                throw new RuntimeException("No entity found with type '" + key.getEntityType() + " ' and name '" + key.getEntityName() + "'.");
 | 
			
		||||
            }
 | 
			
		||||
            return entityContainer;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void processSearchDirection(TbMsg msg, EntityContainer entityContainer) {
 | 
			
		||||
        if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
 | 
			
		||||
            fromId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
 | 
			
		||||
            toId = msg.getOriginator();
 | 
			
		||||
        } else {
 | 
			
		||||
            toId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
 | 
			
		||||
            fromId = msg.getOriginator();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    @AllArgsConstructor
 | 
			
		||||
    private static class Entitykey {
 | 
			
		||||
        private String entityName;
 | 
			
		||||
        private String type;
 | 
			
		||||
        private EntityType entityType;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class EntityCacheLoader extends CacheLoader<Entitykey, EntityContainer> {
 | 
			
		||||
 | 
			
		||||
        private final TbContext ctx;
 | 
			
		||||
        private final boolean createIfNotExists;
 | 
			
		||||
 | 
			
		||||
        private EntityCacheLoader(TbContext ctx, boolean createIfNotExists) {
 | 
			
		||||
            this.ctx = ctx;
 | 
			
		||||
            this.createIfNotExists = createIfNotExists;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public EntityContainer load(Entitykey key) {
 | 
			
		||||
            return loadEntity(key);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private EntityContainer loadEntity(Entitykey entitykey) {
 | 
			
		||||
            EntityType type = entitykey.getEntityType();
 | 
			
		||||
            EntityContainer targetEntity = new EntityContainer();
 | 
			
		||||
            targetEntity.setEntityType(type);
 | 
			
		||||
            switch (type) {
 | 
			
		||||
                case DEVICE:
 | 
			
		||||
                    DeviceService deviceService = ctx.getDeviceService();
 | 
			
		||||
                    Device device = deviceService.findDeviceByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
 | 
			
		||||
                    if (device != null) {
 | 
			
		||||
                        targetEntity.setEntityId(device.getId());
 | 
			
		||||
                    } else if (createIfNotExists) {
 | 
			
		||||
                        Device newDevice = new Device();
 | 
			
		||||
                        newDevice.setName(entitykey.getEntityName());
 | 
			
		||||
                        newDevice.setType(entitykey.getType());
 | 
			
		||||
                        newDevice.setTenantId(ctx.getTenantId());
 | 
			
		||||
                        Device savedDevice = deviceService.saveDevice(newDevice);
 | 
			
		||||
                        targetEntity.setEntityId(savedDevice.getId());
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case ASSET:
 | 
			
		||||
                    AssetService assetService = ctx.getAssetService();
 | 
			
		||||
                    Asset asset = assetService.findAssetByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
 | 
			
		||||
                    if (asset != null) {
 | 
			
		||||
                        targetEntity.setEntityId(asset.getId());
 | 
			
		||||
                    } else if (createIfNotExists) {
 | 
			
		||||
                        Asset newAsset = new Asset();
 | 
			
		||||
                        newAsset.setName(entitykey.getEntityName());
 | 
			
		||||
                        newAsset.setType(entitykey.getType());
 | 
			
		||||
                        newAsset.setTenantId(ctx.getTenantId());
 | 
			
		||||
                        Asset savedAsset = assetService.saveAsset(newAsset);
 | 
			
		||||
                        targetEntity.setEntityId(savedAsset.getId());
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case CUSTOMER:
 | 
			
		||||
                    CustomerService customerService = ctx.getCustomerService();
 | 
			
		||||
                    Optional<Customer> customerOptional = customerService.findCustomerByTenantIdAndTitle(ctx.getTenantId(), entitykey.getEntityName());
 | 
			
		||||
                    if (customerOptional.isPresent()) {
 | 
			
		||||
                        targetEntity.setEntityId(customerOptional.get().getId());
 | 
			
		||||
                    } else if (createIfNotExists) {
 | 
			
		||||
                        Customer newCustomer = new Customer();
 | 
			
		||||
                        newCustomer.setTitle(entitykey.getEntityName());
 | 
			
		||||
                        newCustomer.setTenantId(ctx.getTenantId());
 | 
			
		||||
                        Customer savedCustomer = customerService.saveCustomer(newCustomer);
 | 
			
		||||
                        targetEntity.setEntityId(savedCustomer.getId());
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case TENANT:
 | 
			
		||||
                    targetEntity.setEntityId(ctx.getTenantId());
 | 
			
		||||
                    break;
 | 
			
		||||
                case ENTITY_VIEW:
 | 
			
		||||
                    EntityViewService entityViewService = ctx.getEntityViewService();
 | 
			
		||||
                    EntityView entityView = entityViewService.findEntityViewByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
 | 
			
		||||
                    if (entityView != null) {
 | 
			
		||||
                        targetEntity.setEntityId(entityView.getId());
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case DASHBOARD:
 | 
			
		||||
                    DashboardService dashboardService = ctx.getDashboardService();
 | 
			
		||||
                    TextPageData<DashboardInfo> dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new TextPageLink(200, entitykey.getEntityName()));
 | 
			
		||||
                    for (DashboardInfo dashboardInfo : dashboardInfoTextPageData.getData()) {
 | 
			
		||||
                        if (dashboardInfo.getTitle().equals(entitykey.getEntityName())) {
 | 
			
		||||
                            targetEntity.setEntityId(dashboardInfo.getId());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    return targetEntity;
 | 
			
		||||
            }
 | 
			
		||||
            return targetEntity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,32 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public abstract class TbAbstractRelationActionNodeConfiguration {
 | 
			
		||||
 | 
			
		||||
    private String direction;
 | 
			
		||||
    private String relationType;
 | 
			
		||||
 | 
			
		||||
    private String entityType;
 | 
			
		||||
    private String entityNamePattern;
 | 
			
		||||
    private String entityTypePattern;
 | 
			
		||||
 | 
			
		||||
    private long entityCacheExpiration;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -52,6 +52,10 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssig
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
 | 
			
		||||
        processAssign(ctx, msg, customerId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void processAssign(TbContext ctx, TbMsg msg, CustomerId customerId) {
 | 
			
		||||
        EntityType originatorType = msg.getOriginator().getEntityType();
 | 
			
		||||
        switch (originatorType) {
 | 
			
		||||
            case DEVICE:
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.google.common.base.Function;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.plugin.ComponentType;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RuleNode(
 | 
			
		||||
        type = ComponentType.ACTION,
 | 
			
		||||
@ -49,6 +52,8 @@ import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
)
 | 
			
		||||
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    private static ObjectMapper mapper = new ObjectMapper();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
 | 
			
		||||
@ -56,32 +61,59 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
 | 
			
		||||
        return Futures.transformAsync(latest, a -> {
 | 
			
		||||
            if (a == null || a.getStatus().isCleared()) {
 | 
			
		||||
                return createNewAlarm(ctx, msg);
 | 
			
		||||
        String alarmType;
 | 
			
		||||
        final Alarm msgAlarm;
 | 
			
		||||
 | 
			
		||||
        if (!config.isUseMessageAlarmData()) {
 | 
			
		||||
            alarmType = config.getAlarmType();
 | 
			
		||||
            msgAlarm = null;
 | 
			
		||||
        } else {
 | 
			
		||||
                return updateAlarm(ctx, msg, a);
 | 
			
		||||
            try {
 | 
			
		||||
                msgAlarm = mapper.readValue(msg.getData(), Alarm.class);
 | 
			
		||||
                msgAlarm.setTenantId(ctx.getTenantId());
 | 
			
		||||
                alarmType = msgAlarm.getType();
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                ctx.tellFailure(msg, e);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType);
 | 
			
		||||
        return Futures.transformAsync(latest, existingAlarm -> {
 | 
			
		||||
            if (existingAlarm == null || existingAlarm.getStatus().isCleared()) {
 | 
			
		||||
                return createNewAlarm(ctx, msg, msgAlarm);
 | 
			
		||||
            } else {
 | 
			
		||||
                return updateAlarm(ctx, msg, existingAlarm, msgAlarm);
 | 
			
		||||
            }
 | 
			
		||||
        }, ctx.getDbCallbackExecutor());
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
 | 
			
		||||
    private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) {
 | 
			
		||||
        ListenableFuture<Alarm> asyncAlarm;
 | 
			
		||||
        if (msgAlarm != null ) {
 | 
			
		||||
            asyncAlarm = Futures.immediateCheckedFuture(msgAlarm);
 | 
			
		||||
        } else {
 | 
			
		||||
            asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
 | 
			
		||||
                    details -> buildAlarm(msg, details, ctx.getTenantId()));
 | 
			
		||||
        }
 | 
			
		||||
        ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
 | 
			
		||||
                alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
 | 
			
		||||
        return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
 | 
			
		||||
        ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
 | 
			
		||||
            alarm.setSeverity(config.getSeverity());
 | 
			
		||||
            alarm.setPropagate(config.isPropagate());
 | 
			
		||||
            alarm.setDetails(details);
 | 
			
		||||
            alarm.setEndTs(System.currentTimeMillis());
 | 
			
		||||
            return ctx.getAlarmService().createOrUpdateAlarm(alarm);
 | 
			
		||||
    private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) {
 | 
			
		||||
        ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
 | 
			
		||||
            if (msgAlarm != null) {
 | 
			
		||||
                existingAlarm.setSeverity(msgAlarm.getSeverity());
 | 
			
		||||
                existingAlarm.setPropagate(msgAlarm.isPropagate());
 | 
			
		||||
            } else {
 | 
			
		||||
                existingAlarm.setSeverity(config.getSeverity());
 | 
			
		||||
                existingAlarm.setPropagate(config.isPropagate());
 | 
			
		||||
            }
 | 
			
		||||
            existingAlarm.setDetails(details);
 | 
			
		||||
            existingAlarm.setEndTs(System.currentTimeMillis());
 | 
			
		||||
            return ctx.getAlarmService().createOrUpdateAlarm(existingAlarm);
 | 
			
		||||
        }, ctx.getDbCallbackExecutor());
 | 
			
		||||
 | 
			
		||||
        return Futures.transform(asyncUpdated, a -> new AlarmResult(false, true, false, a));
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
 | 
			
		||||
 | 
			
		||||
    private AlarmSeverity severity;
 | 
			
		||||
    private boolean propagate;
 | 
			
		||||
    private boolean useMessageAlarmData;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbCreateAlarmNodeConfiguration defaultConfiguration() {
 | 
			
		||||
@ -36,6 +37,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
 | 
			
		||||
        configuration.setAlarmType("General Alarm");
 | 
			
		||||
        configuration.setSeverity(AlarmSeverity.CRITICAL);
 | 
			
		||||
        configuration.setPropagate(false);
 | 
			
		||||
        configuration.setUseMessageAlarmData(false);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,156 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RuleNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeException;
 | 
			
		||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 | 
			
		||||
import org.thingsboard.rule.engine.util.EntityContainer;
 | 
			
		||||
import org.thingsboard.server.common.data.id.AssetId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DashboardId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityViewId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.plugin.ComponentType;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RuleNode(
 | 
			
		||||
        type = ComponentType.ACTION,
 | 
			
		||||
        name = "create relation",
 | 
			
		||||
        configClazz = TbCreateRelationNodeConfiguration.class,
 | 
			
		||||
        nodeDescription = "Finds target Entity by entity name pattern and (entity type pattern for Asset, Device) and then create a relation to Originator Entity by type and direction." +
 | 
			
		||||
                " If Selected entity type: Asset, Device or Customer will create new Entity if it doesn't exist and 'Create new entity if not exists' is set to true.",
 | 
			
		||||
        nodeDetails = "If the relation already exists or successfully created -  Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
 | 
			
		||||
        uiResources = {"static/rulenode/rulenode-core-config.js"},
 | 
			
		||||
        configDirective = "tbActionNodeCreateRelationConfig",
 | 
			
		||||
        icon = "add_circle"
 | 
			
		||||
)
 | 
			
		||||
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbCreateRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        return TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean createEntityIfNotExists() {
 | 
			
		||||
        return config.isCreateEntityIfNotExists();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity) {
 | 
			
		||||
        return createIfAbsent(ctx, msg, entity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
 | 
			
		||||
        processSearchDirection(msg, entityContainer);
 | 
			
		||||
        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
 | 
			
		||||
                result -> {
 | 
			
		||||
                    if (!result) {
 | 
			
		||||
                        return processCreateRelation(ctx, entityContainer);
 | 
			
		||||
                    }
 | 
			
		||||
                    return Futures.immediateFuture(true);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        switch (entityContainer.getEntityType()) {
 | 
			
		||||
            case ASSET:
 | 
			
		||||
                return processAsset(ctx, entityContainer);
 | 
			
		||||
            case DEVICE:
 | 
			
		||||
                return processDevice(ctx, entityContainer);
 | 
			
		||||
            case CUSTOMER:
 | 
			
		||||
                return processCustomer(ctx, entityContainer);
 | 
			
		||||
            case DASHBOARD:
 | 
			
		||||
                return processDashboard(ctx, entityContainer);
 | 
			
		||||
            case ENTITY_VIEW:
 | 
			
		||||
                return processView(ctx, entityContainer);
 | 
			
		||||
            case TENANT:
 | 
			
		||||
                return processTenant(ctx, entityContainer);
 | 
			
		||||
        }
 | 
			
		||||
        return Futures.immediateFuture(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
 | 
			
		||||
            if (entityView != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> {
 | 
			
		||||
            if (device != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
 | 
			
		||||
            if (asset != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
 | 
			
		||||
            if (customer != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
 | 
			
		||||
            if (dashboard != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer) {
 | 
			
		||||
        return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), new TenantId(entityContainer.getEntityId().getId())), tenant -> {
 | 
			
		||||
            if (tenant != null) {
 | 
			
		||||
                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
 | 
			
		||||
            } else {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbCreateRelationNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    private boolean createEntityIfNotExists;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbCreateRelationNodeConfiguration defaultConfiguration() {
 | 
			
		||||
        TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
 | 
			
		||||
        configuration.setDirection(EntitySearchDirection.FROM.name());
 | 
			
		||||
        configuration.setRelationType("Contains");
 | 
			
		||||
        configuration.setEntityNamePattern("");
 | 
			
		||||
        configuration.setEntityCacheExpiration(300);
 | 
			
		||||
        configuration.setCreateEntityIfNotExists(false);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RuleNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeException;
 | 
			
		||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 | 
			
		||||
import org.thingsboard.rule.engine.util.EntityContainer;
 | 
			
		||||
import org.thingsboard.server.common.data.plugin.ComponentType;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RuleNode(
 | 
			
		||||
        type = ComponentType.ACTION,
 | 
			
		||||
        name = "delete relation",
 | 
			
		||||
        configClazz = TbDeleteRelationNodeConfiguration.class,
 | 
			
		||||
        nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction.",
 | 
			
		||||
        nodeDetails = "If the relation successfully deleted -  Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
 | 
			
		||||
        uiResources = {"static/rulenode/rulenode-core-config.js"},
 | 
			
		||||
        configDirective = "tbActionNodeDeleteRelationConfig",
 | 
			
		||||
        icon = "remove_circle"
 | 
			
		||||
)
 | 
			
		||||
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbDeleteRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        return TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean createEntityIfNotExists() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
 | 
			
		||||
        return deleteIfExist(ctx, msg, entityContainer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> deleteIfExist(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
 | 
			
		||||
        processSearchDirection(msg, entityContainer);
 | 
			
		||||
        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
 | 
			
		||||
                result -> {
 | 
			
		||||
                    if (result) {
 | 
			
		||||
                        return processDeleteRelation(ctx);
 | 
			
		||||
                    }
 | 
			
		||||
                    return Futures.immediateFuture(true);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Boolean> processDeleteRelation(TbContext ctx) {
 | 
			
		||||
        return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,35 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.action;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbDeleteRelationNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbDeleteRelationNodeConfiguration defaultConfiguration() {
 | 
			
		||||
        TbDeleteRelationNodeConfiguration configuration = new TbDeleteRelationNodeConfiguration();
 | 
			
		||||
        configuration.setDirection(EntitySearchDirection.FROM.name());
 | 
			
		||||
        configuration.setRelationType("Contains");
 | 
			
		||||
        configuration.setEntityNamePattern("");
 | 
			
		||||
        configuration.setEntityCacheExpiration(300);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -21,8 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ArrayNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.rule.engine.api.*;
 | 
			
		||||
import org.apache.commons.lang3.math.NumberUtils;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RuleNode;
 | 
			
		||||
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.rule.engine.api.util.DonAsynchron;
 | 
			
		||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 | 
			
		||||
@ -37,7 +44,9 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
 | 
			
		||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*;
 | 
			
		||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL;
 | 
			
		||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST;
 | 
			
		||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE;
 | 
			
		||||
import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -50,6 +59,7 @@ import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
 | 
			
		||||
        nodeDescription = "Add Message Originator Telemetry for selected time range into Message Metadata\n",
 | 
			
		||||
        nodeDetails = "The node allows you to select fetch mode <b>FIRST/LAST/ALL</b> to fetch telemetry of certain time range that are added into Message metadata without any prefix. " +
 | 
			
		||||
                "If selected fetch mode <b>ALL</b> Telemetry will be added like array into Message Metadata where <b>key</b> is Timestamp and <b>value</b> is value of Telemetry. " +
 | 
			
		||||
                "<b>Note</b>: The maximum size of the fetched array is 1000 records. " +
 | 
			
		||||
                "If selected fetch mode <b>FIRST</b> or <b>LAST</b> Telemetry will be added like string without Timestamp",
 | 
			
		||||
        uiResources = {"static/rulenode/rulenode-core-config.js"},
 | 
			
		||||
        configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
 | 
			
		||||
@ -57,8 +67,6 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
 | 
			
		||||
    private TbGetTelemetryNodeConfiguration config;
 | 
			
		||||
    private List<String> tsKeyNames;
 | 
			
		||||
    private long startTsOffset;
 | 
			
		||||
    private long endTsOffset;
 | 
			
		||||
    private int limit;
 | 
			
		||||
    private ObjectMapper mapper;
 | 
			
		||||
    private String fetchMode;
 | 
			
		||||
@ -67,8 +75,6 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class);
 | 
			
		||||
        tsKeyNames = config.getLatestTsKeyNames();
 | 
			
		||||
        startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval());
 | 
			
		||||
        endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval());
 | 
			
		||||
        limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1;
 | 
			
		||||
        fetchMode = config.getFetchMode();
 | 
			
		||||
        mapper = new ObjectMapper();
 | 
			
		||||
@ -82,8 +88,10 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
            ctx.tellFailure(msg, new IllegalStateException("Telemetry is not selected!"));
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                List<ReadTsKvQuery> queries = buildQueries();
 | 
			
		||||
                ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), queries);
 | 
			
		||||
                if (config.isUseMetadataIntervalPatterns()) {
 | 
			
		||||
                    checkMetadataKeyPatterns(msg);
 | 
			
		||||
                }
 | 
			
		||||
                ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(msg));
 | 
			
		||||
                DonAsynchron.withCallback(list, data -> {
 | 
			
		||||
                    process(data, msg);
 | 
			
		||||
                    TbMsg newMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData());
 | 
			
		||||
@ -95,10 +103,12 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<ReadTsKvQuery> buildQueries() {
 | 
			
		||||
        long ts = System.currentTimeMillis();
 | 
			
		||||
        long startTs = ts - startTsOffset;
 | 
			
		||||
        long endTs = ts - endTsOffset;
 | 
			
		||||
    @Override
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<ReadTsKvQuery> buildQueries(TbMsg msg) {
 | 
			
		||||
        String orderBy;
 | 
			
		||||
        if (fetchMode.equals(FETCH_MODE_FIRST) || fetchMode.equals(FETCH_MODE_ALL)) {
 | 
			
		||||
            orderBy = "ASC";
 | 
			
		||||
@ -106,7 +116,7 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
            orderBy = "DESC";
 | 
			
		||||
        }
 | 
			
		||||
        return tsKeyNames.stream()
 | 
			
		||||
                .map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy))
 | 
			
		||||
                .map(key -> new BaseReadTsKvQuery(key, getInterval(msg).getStartTs(), getInterval(msg).getEndTs(), 1, limit, NONE, orderBy))
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -162,8 +172,79 @@ public class TbGetTelemetryNode implements TbNode {
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
    private Interval getInterval(TbMsg msg) {
 | 
			
		||||
        Interval interval = new Interval();
 | 
			
		||||
        if (config.isUseMetadataIntervalPatterns()) {
 | 
			
		||||
            if (isParsable(msg, config.getStartIntervalPattern())) {
 | 
			
		||||
                interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg.getMetaData())));
 | 
			
		||||
            }
 | 
			
		||||
            if (isParsable(msg, config.getEndIntervalPattern())) {
 | 
			
		||||
                interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg.getMetaData())));
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            long ts = System.currentTimeMillis();
 | 
			
		||||
            interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()));
 | 
			
		||||
            interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()));
 | 
			
		||||
        }
 | 
			
		||||
        return interval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isParsable(TbMsg msg, String pattern) {
 | 
			
		||||
        return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg.getMetaData()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkMetadataKeyPatterns(TbMsg msg) {
 | 
			
		||||
        isUndefined(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
 | 
			
		||||
        isInvalid(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void isUndefined(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
 | 
			
		||||
        if (getMetadataValue(msg, startIntervalPattern) == null && getMetadataValue(msg, endIntervalPattern) == null) {
 | 
			
		||||
            throw new IllegalArgumentException("Message metadata values: '" +
 | 
			
		||||
                    replaceRegex(startIntervalPattern) + "' and '" +
 | 
			
		||||
                    replaceRegex(endIntervalPattern) + "' are undefined");
 | 
			
		||||
        } else {
 | 
			
		||||
            if (getMetadataValue(msg, startIntervalPattern) == null) {
 | 
			
		||||
                throw new IllegalArgumentException("Message metadata value: '" +
 | 
			
		||||
                        replaceRegex(startIntervalPattern) + "' is undefined");
 | 
			
		||||
            }
 | 
			
		||||
            if (getMetadataValue(msg, endIntervalPattern) == null) {
 | 
			
		||||
                throw new IllegalArgumentException("Message metadata value: '" +
 | 
			
		||||
                        replaceRegex(endIntervalPattern) + "' is undefined");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void isInvalid(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
 | 
			
		||||
        if (getInterval(msg).getStartTs() == null && getInterval(msg).getEndTs() == null) {
 | 
			
		||||
            throw new IllegalArgumentException("Message metadata values: '" +
 | 
			
		||||
                    replaceRegex(startIntervalPattern) + "' and '" +
 | 
			
		||||
                    replaceRegex(endIntervalPattern) + "' have invalid format");
 | 
			
		||||
        } else {
 | 
			
		||||
            if (getInterval(msg).getStartTs() == null) {
 | 
			
		||||
                throw new IllegalArgumentException("Message metadata value: '" +
 | 
			
		||||
                        replaceRegex(startIntervalPattern) + "' has invalid format");
 | 
			
		||||
            }
 | 
			
		||||
            if (getInterval(msg).getEndTs() == null) {
 | 
			
		||||
                throw new IllegalArgumentException("Message metadata value: '" +
 | 
			
		||||
                        replaceRegex(endIntervalPattern) + "' has invalid format");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getMetadataValue(TbMsg msg, String pattern) {
 | 
			
		||||
        return msg.getMetaData().getValue(replaceRegex(pattern));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String replaceRegex(String pattern) {
 | 
			
		||||
        return pattern.replaceAll("[${}]", "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    @NoArgsConstructor
 | 
			
		||||
    private static class Interval {
 | 
			
		||||
        private Long startTs;
 | 
			
		||||
        private Long endTs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,12 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
 | 
			
		||||
 | 
			
		||||
    private int startInterval;
 | 
			
		||||
    private int endInterval;
 | 
			
		||||
 | 
			
		||||
    private String startIntervalPattern;
 | 
			
		||||
    private String endIntervalPattern;
 | 
			
		||||
 | 
			
		||||
    private boolean useMetadataIntervalPatterns;
 | 
			
		||||
 | 
			
		||||
    private String startIntervalTimeUnit;
 | 
			
		||||
    private String endIntervalTimeUnit;
 | 
			
		||||
    private String fetchMode; //FIRST, LAST, LATEST
 | 
			
		||||
@ -52,6 +58,9 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
 | 
			
		||||
        configuration.setStartInterval(2);
 | 
			
		||||
        configuration.setEndIntervalTimeUnit(TimeUnit.MINUTES.name());
 | 
			
		||||
        configuration.setEndInterval(1);
 | 
			
		||||
        configuration.setUseMetadataIntervalPatterns(false);
 | 
			
		||||
        configuration.setStartIntervalPattern("");
 | 
			
		||||
        configuration.setEndIntervalPattern("");
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.util;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class EntityContainer {
 | 
			
		||||
 | 
			
		||||
    private EntityId entityId;
 | 
			
		||||
    private EntityType entityType;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -23,7 +23,6 @@
 | 
			
		||||
        <version>2.3.0-SNAPSHOT</version>
 | 
			
		||||
        <artifactId>thingsboard</artifactId>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <groupId>org.thingsboard</groupId>
 | 
			
		||||
    <artifactId>tools</artifactId>
 | 
			
		||||
    <packaging>jar</packaging>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
@ -107,6 +108,27 @@ public class RestClient implements ClientHttpRequestInterceptor {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) {
 | 
			
		||||
        Map<String, String> params = new HashMap<>();
 | 
			
		||||
        params.put("accessToken", accessToken);
 | 
			
		||||
        params.put("clientKeys", clientKeys);
 | 
			
		||||
        params.put("sharedKeys", sharedKeys);
 | 
			
		||||
        try {
 | 
			
		||||
            ResponseEntity<JsonNode> telemetryEntity = restTemplate.getForEntity(baseURL + "/api/v1/{accessToken}/attributes?clientKeys={clientKeys}&sharedKeys={sharedKeys}", JsonNode.class, params);
 | 
			
		||||
            return Optional.of(telemetryEntity.getBody());
 | 
			
		||||
        } catch (HttpClientErrorException exception) {
 | 
			
		||||
            if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
 | 
			
		||||
                return Optional.empty();
 | 
			
		||||
            } else {
 | 
			
		||||
                throw exception;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Customer createCustomer(Customer customer) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Customer createCustomer(String title) {
 | 
			
		||||
        Customer customer = new Customer();
 | 
			
		||||
        customer.setTitle(title);
 | 
			
		||||
@ -120,6 +142,25 @@ public class RestClient implements ClientHttpRequestInterceptor {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DeviceCredentials updateDeviceCredentials(DeviceId deviceId, String token) {
 | 
			
		||||
        DeviceCredentials deviceCredentials = getCredentials(deviceId);
 | 
			
		||||
        deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
 | 
			
		||||
        deviceCredentials.setCredentialsId(token);
 | 
			
		||||
        return saveDeviceCredentials(deviceCredentials);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Device createDevice(Device device) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Asset createAsset(Asset asset) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Asset createAsset(String name, String type) {
 | 
			
		||||
        Asset asset = new Asset();
 | 
			
		||||
        asset.setName(name);
 | 
			
		||||
@ -131,6 +172,18 @@ public class RestClient implements ClientHttpRequestInterceptor {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteCustomer(CustomerId customerId) {
 | 
			
		||||
        restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteDevice(DeviceId deviceId) {
 | 
			
		||||
        restTemplate.delete(baseURL + "/api/device/{deviceId}", deviceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteAsset(AssetId assetId) {
 | 
			
		||||
        restTemplate.delete(baseURL + "/api/asset/{assetId}", assetId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
 | 
			
		||||
                customerId.toString(), deviceId.toString()).getBody();
 | 
			
		||||
 | 
			
		||||
@ -648,6 +648,12 @@ export default class Subscription {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
 | 
			
		||||
        for (var x = 0; x < this.datasourceListeners.length; x++) {
 | 
			
		||||
            this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
 | 
			
		||||
            if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) {
 | 
			
		||||
                this.datasources[x].dataReceived = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.notifyDataLoaded();
 | 
			
		||||
        var update = true;
 | 
			
		||||
        var currentData;
 | 
			
		||||
 | 
			
		||||
@ -31,14 +31,35 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
 | 
			
		||||
 | 
			
		||||
    var linker = function(scope, element) {
 | 
			
		||||
 | 
			
		||||
        function TcpTransport() {
 | 
			
		||||
            this.type = "tcp",
 | 
			
		||||
            this.host = "localhost",
 | 
			
		||||
            this.port = 502,
 | 
			
		||||
            this.timeout = 5000,
 | 
			
		||||
            this.reconnect = true,
 | 
			
		||||
            this.rtuOverTcp = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function UdpTransport() {
 | 
			
		||||
            this.type = "udp",
 | 
			
		||||
            this.host = "localhost",
 | 
			
		||||
            this.port = 502,
 | 
			
		||||
            this.timeout = 5000
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function RtuTransport() {
 | 
			
		||||
            this.type = "rtu",
 | 
			
		||||
            this.portName = "COM1",
 | 
			
		||||
            this.encoding = "ascii",
 | 
			
		||||
            this.timeout = 5000,
 | 
			
		||||
            this.baudRate = 115200,
 | 
			
		||||
            this.dataBits = 7,
 | 
			
		||||
            this.stopBits = 1,
 | 
			
		||||
            this.parity ="even"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function Server() {
 | 
			
		||||
            this.transport = {
 | 
			
		||||
                "type": "tcp",
 | 
			
		||||
                "host": "localhost",
 | 
			
		||||
                "port": 502,
 | 
			
		||||
                "timeout": 3000
 | 
			
		||||
            };
 | 
			
		||||
            this.transport = new TcpTransport();
 | 
			
		||||
            this.devices = []
 | 
			
		||||
        }
 | 
			
		||||
		
 | 
			
		||||
@ -105,9 +126,13 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
 | 
			
		||||
        scope.onTransportChanged = function(server) {
 | 
			
		||||
            var type = server.transport.type;
 | 
			
		||||
 | 
			
		||||
            server.transport = {};
 | 
			
		||||
            server.transport.type = type;
 | 
			
		||||
            server.transport.timeout = 3000;
 | 
			
		||||
            if (type === "tcp") {
 | 
			
		||||
                server.transport = new TcpTransport();
 | 
			
		||||
            } else if (type === "udp") {
 | 
			
		||||
                server.transport = new UdpTransport();
 | 
			
		||||
            } else if (type === "rtu") {
 | 
			
		||||
                server.transport = new RtuTransport();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            scope.theForm.$setDirty();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,8 @@
 | 
			
		||||
                                            
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        
 | 
			
		||||
                                        <div layout="row" ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
 | 
			
		||||
                                        <div ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
 | 
			
		||||
                                            <div layout="row">
 | 
			
		||||
                                                <md-input-container flex="33" class="md-block">
 | 
			
		||||
                                                    <label translate>extension.host</label>
 | 
			
		||||
                                                    <input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
 | 
			
		||||
@ -118,6 +119,25 @@
 | 
			
		||||
                                                </md-input-container>
 | 
			
		||||
                                            </div>
 | 
			
		||||
 | 
			
		||||
                                            <div layout="row" ng-if="server.transport.type == 'tcp'">
 | 
			
		||||
                                                <md-input-container flex="50" class="md-block">
 | 
			
		||||
                                                    <md-checkbox aria-label="{{ 'extension.modbus-tcp-reconnect' | translate }}"
 | 
			
		||||
                                                                 ng-checked="server.transport.reconnect"
 | 
			
		||||
                                                                 name="transportTcpReconnect_{{serverIndex}}"
 | 
			
		||||
                                                                 ng-model="server.transport.reconnect">{{ 'extension.modbus-tcp-reconnect' | translate }}
 | 
			
		||||
                                                    </md-checkbox>
 | 
			
		||||
                                                </md-input-container>
 | 
			
		||||
 | 
			
		||||
                                                <md-input-container flex="50" class="md-block">
 | 
			
		||||
                                                    <md-checkbox aria-label="{{ 'extension.modbus-rtu-over-tcp' | translate }}"
 | 
			
		||||
                                                                 ng-checked="server.transport.rtuOverTcp"
 | 
			
		||||
                                                                 name="transportRtuOverTcp_{{serverIndex}}"
 | 
			
		||||
                                                                 ng-model="server.transport.rtuOverTcp">{{ 'extension.modbus-rtu-over-tcp' | translate }}
 | 
			
		||||
                                                    </md-checkbox>
 | 
			
		||||
                                                </md-input-container>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        
 | 
			
		||||
                                        <div ng-if="server.transport.type == 'rtu'">
 | 
			
		||||
                                        
 | 
			
		||||
                                            <div layout="row">
 | 
			
		||||
 | 
			
		||||
@ -261,7 +261,7 @@
 | 
			
		||||
                                                                                </md-input-container>
 | 
			
		||||
                                                                                <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
 | 
			
		||||
                                                                                    <label translate>extension.filter-expression</label>
 | 
			
		||||
                                                                                    <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
 | 
			
		||||
                                                                                    <input name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
 | 
			
		||||
                                                                                    <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
 | 
			
		||||
                                                                                        <div translate ng-message="required">extension.field-required</div>
 | 
			
		||||
                                                                                    </div>
 | 
			
		||||
 | 
			
		||||
@ -1008,6 +1008,8 @@
 | 
			
		||||
        "modbus-add-server": "Add server/slave",
 | 
			
		||||
        "modbus-add-server-prompt": "Please add server/slave",
 | 
			
		||||
        "modbus-transport": "Transport",
 | 
			
		||||
        "modbus-tcp-reconnect": "Automatically reconnect",
 | 
			
		||||
        "modbus-rtu-over-tcp": "RTU over TCP",
 | 
			
		||||
        "modbus-port-name": "Serial port name",
 | 
			
		||||
        "modbus-encoding": "Encoding",
 | 
			
		||||
        "modbus-parity": "Parity",
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -60,6 +60,9 @@ export default class TbMapWidgetV2 {
 | 
			
		||||
 | 
			
		||||
        var minZoomLevel = this.drawRoutes ? 18 : 15;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var initCallback = function() {
 | 
			
		||||
            tbMap.update();
 | 
			
		||||
            tbMap.resize();
 | 
			
		||||
@ -87,6 +90,9 @@ export default class TbMapWidgetV2 {
 | 
			
		||||
        } else if (mapProvider === 'tencent-map') {
 | 
			
		||||
            this.map = new TbTencentMap($element,this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        tbMap.initBounds = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setCallbacks(callbacks) {
 | 
			
		||||
@ -442,6 +448,7 @@ export default class TbMapWidgetV2 {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function updateLocations(data, datasources) {
 | 
			
		||||
 | 
			
		||||
            var locationsChanged = false;
 | 
			
		||||
            var bounds = tbMap.map.createBounds();
 | 
			
		||||
            var dataMap = toLabelValueMap(data, datasources);
 | 
			
		||||
@ -454,7 +461,11 @@ export default class TbMapWidgetV2 {
 | 
			
		||||
                    tbMap.map.extendBoundsWithMarker(bounds, location.marker);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (locationsChanged) {
 | 
			
		||||
             if (locationsChanged && tbMap.initBounds) {
 | 
			
		||||
                tbMap.initBounds = !datasources.every(
 | 
			
		||||
                    function (ds) {
 | 
			
		||||
                        return ds.dataReceived === true;
 | 
			
		||||
                    });
 | 
			
		||||
                tbMap.map.fitBounds(bounds);
 | 
			
		||||
             }
 | 
			
		||||
        }
 | 
			
		||||
@ -477,7 +488,6 @@ export default class TbMapWidgetV2 {
 | 
			
		||||
            content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data);
 | 
			
		||||
            return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.map && this.map.inited() && this.subscription) {
 | 
			
		||||
            if (this.subscription.data) {
 | 
			
		||||
                if (!this.locations) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user