Browse Source

Merge remote-tracking branch 'upstream/master'

pull/585/head
Volodymyr Babak 8 years ago
parent
commit
fad04b0dd0
  1. 54
      application/src/main/data/json/system/widget_bundles/maps.json
  2. 2
      application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
  3. 23
      application/src/main/resources/thingsboard.yml
  4. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  5. 8
      dao/pom.xml
  6. 24
      dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java
  7. 4
      dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java
  8. 91
      dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
  9. 20
      dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
  10. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  11. 36
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  12. 179
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  13. 44
      dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
  14. 37
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsInsertExecutorType.java
  15. 17
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
  16. 101
      dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
  17. 23
      dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java
  18. 23
      dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java
  19. 11
      dao/src/test/resources/application-test.properties
  20. 5
      dao/src/test/resources/sql-test.properties
  21. 6
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
  22. 9
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
  23. 3
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
  24. 8
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
  25. 4
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
  26. 1
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
  27. 1
      extensions-core/src/main/proto/telemetry.proto
  28. 6
      pom.xml
  29. 4
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  30. 2
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  31. 19
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  32. 8
      ui/src/app/app.run.js
  33. 4
      ui/src/app/common/utils.service.js
  34. 1
      ui/src/app/locale/locale.constant.js
  35. 6
      ui/src/app/widget/lib/entities-table-widget.js
  36. 81
      ui/src/app/widget/lib/flot-widget.js
  37. 2
      ui/src/app/widget/lib/map-widget.js
  38. 51
      ui/src/app/widget/lib/map-widget2.js
  39. 391
      ui/src/app/widget/lib/tencent-map.js
  40. 22
      ui/src/app/widget/lib/timeseries-table-widget.tpl.html

54
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

2
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java

@ -66,6 +66,8 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
//Dump devices, assets and relations
cluster.getSession();
KeyspaceMetadata ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());
log.info("Dumping devices ...");

23
application/src/main/resources/thingsboard.yml

@ -77,6 +77,8 @@ http:
# MQTT server parameters
mqtt:
# Enable/disable mqtt transport protocol.
enabled: "${MQTT_ENABLED:true}"
bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
bind_port: "${MQTT_BIND_PORT:1883}"
adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}"
@ -102,6 +104,8 @@ mqtt:
# CoAP server parameters
coap:
# Enable/disable coap transport protocol.
enabled: "${COAP_ENABLED:true}"
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
@ -157,6 +161,13 @@ cassandra:
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# SQL configuration parameters
sql:
# Specify executor service type used to perform timeseries insert tasks: SINGLE FIXED CACHED
ts_inserts_executor_type: "${SQL_TS_INSERTS_EXECUTOR_TYPE:fixed}"
# Specify thread pool size for FIXED executor service type
ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:10}"
# Actor system parameters
actors:
tenant:
@ -201,6 +212,18 @@ cache:
policy: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_POLICY:PER_NODE}"
size: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_SIZE:1000000}"
caching:
specs:
relations:
timeToLiveInMinutes: 1440
maxSize: 100000
deviceCredentials:
timeToLiveInMinutes: 1440
maxSize: 100000
devices:
timeToLiveInMinutes: 1440
maxSize: 100000
# Check new version updates parameters
updates:
# Enable/disable updates checking.

2
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -17,4 +17,6 @@ package org.thingsboard.server.common.data;
public class CacheConstants {
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations";
public static final String DEVICE_CACHE = "devices";
}

8
dao/pom.xml

@ -148,6 +148,10 @@
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
@ -174,6 +178,10 @@
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

24
dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.cache;
import lombok.Data;
@Data
public class CacheSpecs {
private Integer timeToLiveInMinutes;
private Integer maxSize;
}

4
dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java

@ -23,6 +23,8 @@ import java.lang.reflect.Method;
public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator {
private static final String NOT_VALID_DEVICE = "notValidDeviceCredentialsId";
@Override
public Object generate(Object o, Method method, Object... objects) {
DeviceCredentialsService deviceCredentialsService = (DeviceCredentialsService) o;
@ -33,6 +35,6 @@ public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator {
return oldDeviceCredentials.getCredentialsId();
}
}
return null;
return NOT_VALID_DEVICE;
}
}

91
dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java

@ -15,76 +15,57 @@
*/
package org.thingsboard.server.dao.cache;
import com.hazelcast.config.*;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.instance.GroupProperty;
import com.hazelcast.spring.cache.HazelcastCacheManager;
import com.hazelcast.zookeeper.ZookeeperDiscoveryProperties;
import com.hazelcast.zookeeper.ZookeeperDiscoveryStrategyFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thingsboard.server.common.data.CacheConstants;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Configuration
@ConfigurationProperties(prefix = "caching")
@EnableCaching
@ConditionalOnProperty(prefix = "cache", value = "enabled", havingValue = "true")
@Data
public class ServiceCacheConfiguration {
private static final String HAZELCAST_CLUSTER_NAME = "hazelcast";
@Value("${cache.device_credentials.max_size.size}")
private Integer cacheDeviceCredentialsMaxSizeSize;
@Value("${cache.device_credentials.max_size.policy}")
private String cacheDeviceCredentialsMaxSizePolicy;
@Value("${cache.device_credentials.time_to_live}")
private Integer cacheDeviceCredentialsTTL;
@Value("${zk.enabled}")
private boolean zkEnabled;
@Value("${zk.url}")
private String zkUrl;
@Value("${zk.zk_dir}")
private String zkDir;
private Map<String, CacheSpecs> specs;
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
if (zkEnabled) {
addZkConfig(config);
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
if (specs != null) {
List<CaffeineCache> caches =
specs.entrySet().stream()
.map(entry -> buildCache(entry.getKey(),
entry.getValue()))
.collect(Collectors.toList());
manager.setCaches(caches);
}
config.addMapConfig(createDeviceCredentialsCacheConfig());
return Hazelcast.newHazelcastInstance(config);
return manager;
}
private void addZkConfig(Config config) {
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
config.setProperty(GroupProperty.DISCOVERY_SPI_ENABLED.getName(), Boolean.TRUE.toString());
DiscoveryStrategyConfig discoveryStrategyConfig = new DiscoveryStrategyConfig(new ZookeeperDiscoveryStrategyFactory());
discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.ZOOKEEPER_URL.key(), zkUrl);
discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.ZOOKEEPER_PATH.key(), zkDir);
discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.GROUP.key(), HAZELCAST_CLUSTER_NAME);
config.getNetworkConfig().getJoin().getDiscoveryConfig().addDiscoveryStrategyConfig(discoveryStrategyConfig);
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES)
.maximumSize(cacheSpec.getMaxSize())
.ticker(ticker());
return new CaffeineCache(name, caffeineBuilder.build());
}
private MapConfig createDeviceCredentialsCacheConfig() {
MapConfig deviceCredentialsCacheConfig = new MapConfig(CacheConstants.DEVICE_CREDENTIALS_CACHE);
deviceCredentialsCacheConfig.setTimeToLiveSeconds(cacheDeviceCredentialsTTL);
deviceCredentialsCacheConfig.setEvictionPolicy(EvictionPolicy.LRU);
deviceCredentialsCacheConfig.setMaxSizeConfig(
new MaxSizeConfig(
cacheDeviceCredentialsMaxSizeSize,
MaxSizeConfig.MaxSizePolicy.valueOf(cacheDeviceCredentialsMaxSizePolicy))
);
return deviceCredentialsCacheConfig;
@Bean
public Ticker ticker() {
return Ticker.systemTicker();
}
@Bean
@ -92,8 +73,4 @@ public class ServiceCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator();
}
@Bean
public CacheManager cacheManager() {
return new HazelcastCacheManager(hazelcastInstance());
}
}

20
dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java

@ -75,6 +75,7 @@ public abstract class AbstractCassandraCluster {
private Environment environment;
private Cluster cluster;
private Cluster.Builder clusterBuilder;
@Getter(AccessLevel.NONE) private Session session;
@ -88,29 +89,27 @@ public abstract class AbstractCassandraCluster {
protected void init(String keyspaceName) {
this.keyspaceName = keyspaceName;
Cluster.Builder builder = Cluster.builder()
this.clusterBuilder = Cluster.builder()
.addContactPointsWithPorts(getContactPoints(url))
.withClusterName(clusterName)
.withSocketOptions(socketOpts.getOpts())
.withPoolingOptions(new PoolingOptions()
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 32768));
builder.withQueryOptions(queryOpts.getOpts());
builder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
this.clusterBuilder.withQueryOptions(queryOpts.getOpts());
this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
if (ssl) {
builder.withSSL();
this.clusterBuilder.withSSL();
}
if (!jmx) {
builder.withoutJMXReporting();
this.clusterBuilder.withoutJMXReporting();
}
if (!metrics) {
builder.withoutMetrics();
this.clusterBuilder.withoutMetrics();
}
if (credentials) {
builder.withCredentials(username, password);
this.clusterBuilder.withCredentials(username, password);
}
cluster = builder.build();
cluster.init();
if (!isInstall()) {
initSession();
}
@ -139,7 +138,8 @@ public abstract class AbstractCassandraCluster {
long endTime = System.currentTimeMillis() + initTimeout;
while (System.currentTimeMillis() < endTime) {
try {
cluster = clusterBuilder.build();
cluster.init();
if (this.keyspaceName != null) {
session = cluster.connect(keyspaceName);
} else {

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -34,7 +34,7 @@ public interface DeviceService {
ListenableFuture<Device> findDeviceByIdAsync(DeviceId deviceId);
Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name);
Device findDeviceByTenantIdAndName(TenantId tenantId, String name);
Device saveDevice(Device device);

36
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -22,6 +22,10 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.*;
@ -33,12 +37,12 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
@ -47,6 +51,7 @@ import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.Validator.*;
@ -71,6 +76,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private CacheManager cacheManager;
@Override
public Device findDeviceById(DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
@ -85,18 +93,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findByIdAsync(deviceId.getId());
}
@Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}")
@Override
public Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name) {
public Device findDeviceByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Optional<Device> deviceOpt = deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name);
if (deviceOpt.isPresent()) {
return Optional.of(deviceOpt.get());
} else {
return Optional.empty();
}
return deviceOpt.orElse(null);
}
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}")
@Override
public Device saveDevice(Device device) {
log.trace("Executing saveDevice [{}]", device);
@ -129,12 +135,18 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Override
public void deleteDevice(DeviceId deviceId) {
log.trace("Executing deleteDevice [{}]", deviceId);
Cache cache = cacheManager.getCache(DEVICE_CACHE);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId);
if (deviceCredentials != null) {
deviceCredentialsService.deleteDeviceCredentials(deviceCredentials);
}
deleteEntityRelations(deviceId);
Device device = deviceDao.findById(deviceId.getId());
List<Object> list = new ArrayList<>();
list.add(device.getTenantId());
list.add(device.getName());
cache.evict(list);
deviceDao.removeById(deviceId.getId());
}
@ -190,7 +202,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink);
List<Device> devices = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
List<Device> devices = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
return new TextPageData<>(devices, pageLink);
}
@ -244,10 +256,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
ListenableFuture<List<EntitySubtype>> tenantDeviceTypes = deviceDao.findTenantDeviceTypesAsync(tenantId.getId());
return Futures.transform(tenantDeviceTypes,
(Function<List<EntitySubtype>, List<EntitySubtype>>) deviceTypes -> {
deviceTypes.sort(Comparator.comparing(EntitySubtype::getType));
return deviceTypes;
});
(Function<List<EntitySubtype>, List<EntitySubtype>>) deviceTypes -> {
deviceTypes.sort(Comparator.comparing(EntitySubtype::getType));
return deviceTypes;
});
}
private DataValidator<Device> deviceValidator =

179
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -21,6 +21,11 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
@ -34,6 +39,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
/**
* Created by ashvayka on 28.04.17.
*/
@ -47,6 +54,9 @@ public class BaseRelationService implements RelationService {
@Autowired
private EntityService entityService;
@Autowired
private CacheManager cacheManager;
@Override
public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing checkRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
@ -54,6 +64,7 @@ public class BaseRelationService implements RelationService {
return relationDao.checkRelation(from, to, relationType, typeGroup);
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
@Override
public ListenableFuture<EntityRelation> getRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing EntityRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
@ -61,6 +72,12 @@ public class BaseRelationService implements RelationService {
return relationDao.getRelation(from, to, relationType, typeGroup);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}")
})
@Override
public boolean saveRelation(EntityRelation relation) {
log.trace("Executing saveRelation [{}]", relation);
@ -68,6 +85,12 @@ public class BaseRelationService implements RelationService {
return relationDao.saveRelation(relation);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}")
})
@Override
public ListenableFuture<Boolean> saveRelationAsync(EntityRelation relation) {
log.trace("Executing saveRelationAsync [{}]", relation);
@ -75,6 +98,13 @@ public class BaseRelationService implements RelationService {
return relationDao.saveRelationAsync(relation);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type}")
})
@Override
public boolean deleteRelation(EntityRelation relation) {
log.trace("Executing deleteRelation [{}]", relation);
@ -82,6 +112,13 @@ public class BaseRelationService implements RelationService {
return relationDao.deleteRelation(relation);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
log.trace("Executing deleteRelationAsync [{}]", relation);
@ -89,6 +126,13 @@ public class BaseRelationService implements RelationService {
return relationDao.deleteRelationAsync(relation);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
})
@Override
public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
@ -96,6 +140,13 @@ public class BaseRelationService implements RelationService {
return relationDao.deleteRelation(from, to, relationType, typeGroup);
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup);
@ -105,23 +156,17 @@ public class BaseRelationService implements RelationService {
@Override
public boolean deleteEntityRelations(EntityId entity) {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelations [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new Function<List<List<EntityRelation>>, List<Boolean>>() {
@Override
public List<Boolean> apply(List<List<EntityRelation>> relations) {
List<Boolean> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> results.add(relationDao.deleteRelation(relation)));
}
return results;
}
});
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo, (List<List<EntityRelation>> relations) ->
getBooleans(relations, cache, true));
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
boolean inboundDeleteResult = false;
try {
@ -129,37 +174,105 @@ public class BaseRelationService implements RelationService {
} catch (InterruptedException | ExecutionException e) {
log.error("Error deleting entity inbound relations", e);
}
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByFrom(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transform(inboundRelationsFrom, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
getBooleans(relations, cache, false));
boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity);
return inboundDeleteResult && outboundDeleteResult;
}
private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<Boolean> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> {
checkFromDeleteSync(cache, results, relation, isRemove);
});
}
return results;
}
private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelation(relation));
cacheEviction(relation, relation.getTo(), cache);
} else {
cacheEviction(relation, relation.getFrom(), cache);
}
}
@Override
public ListenableFuture<Boolean> deleteEntityRelationsAsync(EntityId entity) {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<List<EntityRelation>>, List<Boolean>>() {
@Override
public ListenableFuture<List<Boolean>> apply(List<List<EntityRelation>> relations) throws Exception {
List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> results.add(relationDao.deleteRelationAsync(relation)));
}
return Futures.allAsList(results);
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo,
(AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
return Futures.allAsList(results);
});
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelationsAsync(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByTo(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transform(inboundRelationsFrom, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
return Futures.allAsList(results);
});
ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelationsAsync(entity);
return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction());
}
private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> {
checkFromDeleteAsync(cache, results, relation, isRemove);
});
}
return results;
}
private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelationAsync(relation));
cacheEviction(relation, relation.getTo(), cache);
} else {
cacheEviction(relation, relation.getFrom(), cache);
}
}
private void cacheEviction(EntityRelation relation, EntityId entityId, Cache cache) {
cache.evict(entityId);
List<Object> toAndType = new ArrayList<>();
toAndType.add(entityId);
toAndType.add(relation.getType());
cache.evict(toAndType);
List<Object> fromToAndType = new ArrayList<>();
fromToAndType.add(relation.getFrom());
fromToAndType.add(relation.getTo());
fromToAndType.add(relation.getType());
cache.evict(fromToAndType);
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "#from")
@Override
public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from, RelationTypeGroup typeGroup) {
log.trace("Executing findByFrom [{}][{}]", from, typeGroup);
@ -176,17 +289,18 @@ public class BaseRelationService implements RelationService {
ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from, typeGroup);
ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
(AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> relation2.getTo(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
);
return Futures.successfulAsList(futures);
});
});
return relationsInfo;
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}")
@Override
public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
@ -196,6 +310,7 @@ public class BaseRelationService implements RelationService {
return relationDao.findAllByFromAndType(from, relationType, typeGroup);
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "#to")
@Override
public ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup) {
log.trace("Executing findByTo [{}][{}]", to, typeGroup);
@ -214,9 +329,9 @@ public class BaseRelationService implements RelationService {
(AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
futures.add(fetchRelationInfoAsync(relation,
relation2 -> relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
);
return Futures.successfulAsList(futures);
});
@ -236,6 +351,7 @@ public class BaseRelationService implements RelationService {
return entityRelationInfo;
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}")
@Override
public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);
@ -417,5 +533,4 @@ public class BaseRelationService implements RelationService {
}
return relations;
}
}

44
dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java

@ -20,6 +20,7 @@ import com.google.common.collect.Lists;
import com.google.common.util.concurrent.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter;
@ -31,14 +32,17 @@ import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.timeseries.TsInsertExecutorType;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@ -50,7 +54,13 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@SqlDao
public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
@Value("${sql.ts_inserts_executor_type}")
private String insertExecutorType;
@Value("${sql.ts_inserts_fixed_thread_pool_size}")
private int insertFixedThreadPoolSize;
private ListeningExecutorService insertService;
@Autowired
private TsKvRepository tsKvRepository;
@ -58,6 +68,32 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
@Autowired
private TsKvLatestRepository tsKvLatestRepository;
@PostConstruct
public void init() {
Optional<TsInsertExecutorType> executorTypeOptional = TsInsertExecutorType.parse(insertExecutorType);
TsInsertExecutorType executorType;
if (executorTypeOptional.isPresent()) {
executorType = executorTypeOptional.get();
} else {
executorType = TsInsertExecutorType.FIXED;
}
switch (executorType) {
case SINGLE:
insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
break;
case FIXED:
int poolSize = insertFixedThreadPoolSize;
if (poolSize <= 0) {
poolSize = 10;
}
insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize));
break;
case CACHED:
insertService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
break;
}
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries
@ -234,7 +270,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
log.trace("Saving entity: " + entity);
log.trace("Saving entity: {}", entity);
return insertService.submit(() -> {
tsKvRepository.save(entity);
return null;
@ -265,7 +301,9 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
@PreDestroy
void onDestroy() {
insertService.shutdown();
if (insertService != null) {
insertService.shutdown();
}
}
}

37
dao/src/main/java/org/thingsboard/server/dao/timeseries/TsInsertExecutorType.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.timeseries;
import java.util.Optional;
public enum TsInsertExecutorType {
SINGLE,
FIXED,
CACHED;
public static Optional<TsInsertExecutorType> parse(String name) {
TsInsertExecutorType executorType = null;
if (name != null) {
for (TsInsertExecutorType type : TsInsertExecutorType.values()) {
if (type.name().equalsIgnoreCase(name)) {
executorType = type;
break;
}
}
}
return Optional.of(executorType);
}
}

17
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java

@ -15,16 +15,14 @@
*/
package org.thingsboard.server.dao.service;
import com.hazelcast.core.HazelcastInstance;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.springframework.cache.CacheManager;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Device;
@ -40,7 +38,6 @@ import java.util.UUID;
import static org.mockito.Mockito.*;
@TestPropertySource(properties = {"cache.enabled = true"})
public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest {
private static final String CREDENTIALS_ID_1 = RandomStringUtils.randomAlphanumeric(20);
@ -53,7 +50,7 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
private DeviceService deviceService;
@Autowired
private HazelcastInstance hazelcastInstance;
private CacheManager cacheManager;
private UUID deviceId = UUID.randomUUID();
@ -67,7 +64,7 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
@After
public void cleanup() {
hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).evictAll();
cacheManager.getCache(CacheConstants.DEVICE_CREDENTIALS_CACHE).clear();
}
@Test
@ -77,7 +74,6 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
}
@ -88,17 +84,13 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.deleteDeviceCredentials(createDummyDeviceCredentials(CREDENTIALS_ID_1, deviceId));
Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
verify(deviceCredentialsDao, times(2)).findByCredentialsId(CREDENTIALS_ID_1);
}
@ -109,7 +101,6 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
when(deviceCredentialsDao.findByDeviceId(deviceId)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
@ -119,13 +110,11 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest
when(deviceService.findDeviceById(new DeviceId(deviceId))).thenReturn(new Device());
deviceCredentialsService.updateDeviceCredentials(createDummyDeviceCredentials(deviceCredentialsId, CREDENTIALS_ID_2, deviceId));
Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(null);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
verify(deviceCredentialsDao, times(3)).findByCredentialsId(CREDENTIALS_ID_1);
}

101
dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service;
import com.google.common.util.concurrent.Futures;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.test.util.ReflectionTestUtils;
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.relation.RelationTypeGroup;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.mockito.Mockito.*;
import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
public abstract class BaseRelationCacheTest extends AbstractServiceTest {
private static final EntityId ENTITY_ID_FROM = new DeviceId(UUID.randomUUID());
private static final EntityId ENTITY_ID_TO = new DeviceId(UUID.randomUUID());
private static final String RELATION_TYPE = "Contains";
@Autowired
private RelationService relationService;
@Autowired
private CacheManager cacheManager;
private RelationDao relationDao;
@Before
public void setup() throws Exception {
relationDao = mock(RelationDao.class);
ReflectionTestUtils.setField(unwrapRelationService(), "relationDao", relationDao);
}
@After
public void cleanup() {
cacheManager.getCache(RELATIONS_CACHE).clear();
}
private RelationService unwrapRelationService() throws Exception {
if (AopUtils.isAopProxy(relationService) && relationService instanceof Advised) {
Object target = ((Advised) relationService).getTargetSource().getTarget();
return (RelationService) target;
}
return null;
}
@Test
public void testFindRelationByFrom_Cached() throws ExecutionException, InterruptedException {
when(relationDao.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON))
.thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)));
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
verify(relationDao, times(1)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
}
@Test
public void testDeleteRelations_EvictsCache() {
when(relationDao.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON))
.thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)));
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
verify(relationDao, times(1)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.deleteRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
verify(relationDao, times(2)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
}
}

23
dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.nosql;
import org.thingsboard.server.dao.service.BaseRelationCacheTest;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class RelationCacheNoSqlTest extends BaseRelationCacheTest {
}

23
dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.sql;
import org.thingsboard.server.dao.service.BaseRelationCacheTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class RelationCacheSqlTest extends BaseRelationCacheTest {
}

11
dao/src/test/resources/application-test.properties

@ -9,4 +9,13 @@ zk.zk_dir=/thingsboard
updates.enabled=false
audit_log.enabled=true
audit_log.enabled=true
caching.specs.relations.timeToLiveInMinutes=1440
caching.specs.relations.maxSize=100000
caching.specs.deviceCredentials.timeToLiveInMinutes=1440
caching.specs.deviceCredentials.maxSize=100000
caching.specs.devices.timeToLiveInMinutes=1440
caching.specs.devices.maxSize=100000

5
dao/src/test/resources/sql-test.properties

@ -1,4 +1,7 @@
database.type=sql
database.type=sql
sql.ts_inserts_executor_type=fixed
sql.ts_inserts_fixed_thread_pool_size=10
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate

6
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java

@ -108,12 +108,12 @@ public class MailPlugin extends AbstractPlugin<MailPluginConfiguration> implemen
MimeMessage mailMsg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
helper.setFrom(msg.getFrom());
helper.setTo(msg.getTo());
helper.setTo(msg.getTo().split("\\s*,\\s*"));
if (!StringUtils.isEmpty(msg.getCc())) {
helper.setCc(msg.getCc());
helper.setCc(msg.getCc().split("\\s*,\\s*"));
}
if (!StringUtils.isEmpty(msg.getBcc())) {
helper.setBcc(msg.getBcc());
helper.setBcc(msg.getBcc().split("\\s*,\\s*"));
}
helper.setSubject(msg.getSubject());
helper.setText(msg.getBody());

9
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java

@ -33,6 +33,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionU
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* @author Andrew Shvayka
@ -174,9 +175,13 @@ public class SubscriptionManager {
}
public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
onLocalSubscriptionUpdate(ctx, entityId, s -> type == s.getType(), f);
}
public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
if (deviceSubscriptions != null) {
deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> {
deviceSubscriptions.stream().filter(filter).forEach(s -> {
String sessionId = s.getWsSessionId();
List<TsKvEntry> subscriptionUpdate = f.apply(s);
if (!subscriptionUpdate.isEmpty()) {
@ -206,7 +211,7 @@ public class SubscriptionManager {
public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
if (!serverAddress.isPresent()) {
onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.ATTRIBUTES, s -> {
onLocalSubscriptionUpdate(ctx, entityId, s -> SubscriptionType.ATTRIBUTES == s.getType() && scope.equals(s.getScope()), s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
for (AttributeKvEntry kv : attributes) {
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {

3
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java

@ -114,7 +114,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
}
Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
Subscription subscription = new Subscription(
new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap),
new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
false, msg.getServerAddress());
subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
}
@ -127,6 +127,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
builder.setEntityId(cmd.getEntityId().getId().toString());
builder.setType(cmd.getType().name());
builder.setAllKeys(cmd.isAllKeys());
builder.setScope(cmd.getScope());
cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray()));
}

8
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java

@ -131,7 +131,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
keys.forEach(key -> subState.put(key, 0L));
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState);
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@ -168,7 +168,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState);
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size());
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState);
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@ -262,7 +262,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState);
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}

4
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java

@ -51,6 +51,10 @@ public class Subscription {
return getSub().getType();
}
public String getScope() {
return getSub().getScope();
}
public boolean isAllKeys() {
return getSub().isAllKeys();
}

1
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java

@ -33,6 +33,7 @@ public class SubscriptionState {
@Getter private final SubscriptionType type;
@Getter private final boolean allKeys;
@Getter private final Map<String, Long> keyStates;
@Getter private final String scope;
@Override
public boolean equals(Object o) {

1
extensions-core/src/main/proto/telemetry.proto

@ -27,6 +27,7 @@ message SubscriptionProto {
string type = 5;
bool allKeys = 6;
repeated SubscriptionKetStateProto keyStates = 7;
string scope = 8;
}
message SubscriptionUpdateProto {

6
pom.xml

@ -43,6 +43,7 @@
<cassandra-unit.version>3.0.0.1</cassandra-unit.version>
<takari-cpsuite.version>1.2.7</takari-cpsuite.version>
<guava.version>18.0</guava.version>
<caffeine.version>2.6.1</caffeine.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-validator.version>1.5.0</commons-validator.version>
<commons-io.version>2.5</commons-io.version>
@ -644,6 +645,11 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>

4
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java

@ -26,17 +26,17 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service("CoapTransportService")
@ConditionalOnProperty(prefix = "coap", value = "enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class CoapTransportService {

2
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -24,6 +24,7 @@ import io.netty.util.ResourceLeakDetector;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
@ -39,6 +40,7 @@ import javax.annotation.PreDestroy;
* @author Andrew Shvayka
*/
@Service("MqttTransportService")
@ConditionalOnProperty(prefix = "mqtt", value = "enabled", havingValue = "true", matchIfMissing = false)
@Slf4j
public class MqttTransportService {

19
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -84,16 +84,15 @@ public class GatewaySessionCtx {
private void onDeviceConnect(String deviceName, String deviceType) {
if (!devices.containsKey(deviceName)) {
Optional<Device> deviceOpt = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), deviceName);
Device device = deviceOpt.orElseGet(() -> {
Device newDevice = new Device();
newDevice.setTenantId(gateway.getTenantId());
newDevice.setName(deviceName);
newDevice.setType(deviceType);
newDevice = deviceService.saveDevice(newDevice);
relationService.saveRelationAsync(new EntityRelation(gateway.getId(), newDevice.getId(), "Created"));
return newDevice;
});
Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), deviceName);
if (device == null) {
device = new Device();
device.setTenantId(gateway.getTenantId());
device.setName(deviceName);
device.setType(deviceType);
device = deviceService.saveDevice(device);
relationService.saveRelationAsync(new EntityRelation(gateway.getId(), device.getId(), "Created"));
}
GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device);
devices.put(deviceName, ctx);
log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);

8
ui/src/app/app.run.js

@ -20,7 +20,13 @@ import UrlHandler from './url.handler';
export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
$window.Flow = Flow;
var frame = $window.frameElement;
var frame = null;
try {
frame = $window.frameElement;
} catch(e) {
// ie11 fix
}
var unauthorizedDialog = null;
var forbiddenDialog = null;

4
ui/src/app/common/utils.service.js

@ -553,7 +553,9 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
var aspect = imageAspectMap[urlHashCode];
if (angular.isUndefined(aspect)) {
var testImage = document.createElement('img'); // eslint-disable-line
testImage.style.visibility = 'hidden';
testImage.style.position = 'absolute';
testImage.style.left = '-99999px';
testImage.style.top = '-99999px';
testImage.onload = function() {
aspect = testImage.width / testImage.height;
document.body.removeChild(testImage); //eslint-disable-line

1
ui/src/app/locale/locale.constant.js

@ -1214,6 +1214,7 @@ export default angular.module('thingsboard.locale', [])
"remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.",
"timeseries": "Time series",
"search-data": "Search data",
"no-data-found": "No data found",
"latest-values": "Latest values",
"rpc": "Control widget",
"alarm": "Alarm widget",

6
ui/src/app/widget/lib/entities-table-widget.js

@ -340,7 +340,11 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
}
if (!style.width) {
var columnWidth = vm.columnWidth[key.label];
style.width = columnWidth;
if(columnWidth !== "0px") {
style.width = columnWidth;
} else {
style.width = "auto";
}
}
return style;
}

81
ui/src/app/widget/lib/flot-widget.js

@ -238,6 +238,7 @@ export default class TbFlot {
if (this.ticksFormatterFunction) {
return this.ticksFormatterFunction(value);
}
var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1,
formatted = "" + Math.round(value * factor) / factor;
if (this.tickDecimals != null) {
@ -248,9 +249,12 @@ export default class TbFlot {
formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision);
}
}
formatted += ' ' + this.tickUnits;
if (this.tickUnits) {
formatted += ' ' + this.tickUnits;
}
return formatted;
}
};
this.yaxis.tickFormatter = ctx.yAxisTickFormatter;
@ -262,6 +266,16 @@ export default class TbFlot {
this.yaxis.labelFont.color = this.yaxis.font.color;
this.yaxis.labelFont.size = this.yaxis.font.size+2;
this.yaxis.labelFont.weight = "bold";
if (angular.isNumber(settings.yaxis.tickSize)) {
this.yaxis.tickSize = settings.yaxis.tickSize;
} else {
this.yaxis.tickSize = null;
}
if (angular.isNumber(settings.yaxis.tickDecimals)) {
this.yaxis.tickDecimals = settings.yaxis.tickDecimals
} else {
this.yaxis.tickDecimals = null;
}
if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) {
try {
this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter);
@ -405,9 +419,13 @@ export default class TbFlot {
}
}
series.lines = {
fill: keySettings.fillLines === true,
show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
fill: keySettings.fillLines === true
};
if (this.chartType === 'line' || this.chartType === 'state') {
series.lines.show = keySettings.showLines !== false
} else {
series.lines.show = keySettings.showLines === true;
}
if (angular.isDefined(keySettings.lineWidth)) {
series.lines.lineWidth = keySettings.lineWidth;
@ -487,9 +505,19 @@ export default class TbFlot {
createYAxis(keySettings, units) {
var yaxis = angular.copy(this.yaxis);
var tickDecimals, tickSize;
var label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label;
var tickDecimals = angular.isDefined(keySettings.axisTickDecimals) ? keySettings.axisTickDecimals : 0;
if (angular.isNumber(keySettings.axisTickDecimals)) {
tickDecimals = keySettings.axisTickDecimals;
} else {
tickDecimals = yaxis.tickDecimals;
}
if (angular.isNumber(keySettings.axisTickSize)) {
tickSize = keySettings.axisTickSize;
} else {
tickSize = yaxis.tickSize;
}
var position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : "left";
var min = angular.isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min;
@ -500,6 +528,7 @@ export default class TbFlot {
yaxis.max = max;
yaxis.tickUnits = units;
yaxis.tickDecimals = tickDecimals;
yaxis.tickSize = tickSize;
yaxis.alignTicksWithAxis = position == "right" ? 1 : null;
yaxis.position = position;
@ -545,7 +574,7 @@ export default class TbFlot {
}
}
yaxis.hidden = hidden;
var newIndex = -1;
var newIndex = 1;
if (!yaxis.hidden) {
this.options.yaxes.push(yaxis);
newIndex = this.options.yaxes.length;
@ -928,6 +957,16 @@ export default class TbFlot {
"title": "Ticks formatter function, f(value)",
"type": "string",
"default": ""
},
"tickDecimals": {
"title": "The number of decimals to display",
"type": "number",
"default": 0
},
"tickSize": {
"title": "Step size between ticks",
"type": "number",
"default": null
}
}
}
@ -986,6 +1025,8 @@ export default class TbFlot {
"items": [
"yaxis.min",
"yaxis.max",
"yaxis.tickDecimals",
"yaxis.tickSize",
"yaxis.showLabels",
"yaxis.title",
"yaxis.titleAngle",
@ -1010,24 +1051,24 @@ export default class TbFlot {
static datakeySettingsSchema(defaultShowLines) {
return {
"schema": {
"schema": {
"type": "object",
"title": "DataKeySettings",
"properties": {
"title": "DataKeySettings",
"properties": {
"showLines": {
"title": "Show lines",
"type": "boolean",
"default": defaultShowLines
"type": "boolean",
"default": defaultShowLines
},
"fillLines": {
"title": "Fill lines",
"type": "boolean",
"default": false
"type": "boolean",
"default": false
},
"showPoints": {
"title": "Show points",
"type": "boolean",
"default": false
"type": "boolean",
"default": false
},
"tooltipValueFormatter": {
"title": "Tooltip value format function, f(value)",
@ -1059,6 +1100,11 @@ export default class TbFlot {
"type": "number",
"default": 0
},
"axisTickSize": {
"title": "Axis step size between ticks",
"type": "number",
"default": null
},
"axisPosition": {
"title": "Axis position",
"type": "string",
@ -1072,7 +1118,7 @@ export default class TbFlot {
},
"required": ["showLines", "fillLines", "showPoints"]
},
"form": [
"form": [
"showLines",
"fillLines",
"showPoints",
@ -1085,6 +1131,7 @@ export default class TbFlot {
"axisMax",
"axisTitle",
"axisTickDecimals",
"axisTickSize",
{
"key": "axisPosition",
"type": "rc-select",
@ -1401,4 +1448,4 @@ export default class TbFlot {
}
}
/* eslint-enable angular/angularelement */
/* eslint-enable angular/angularelement */

2
ui/src/app/widget/lib/map-widget.js

@ -276,7 +276,7 @@ export default class TbMapWidget {
this.locationsSettings[i].useMarkerImage = true;
var url = this.ctx.settings.markerImage;
var size = this.ctx.settings.markerImageSize || 34;
this.locationSettings.currentImage = {
this.locationsSettings[i].currentImage = {
url: url,
size: size
};

51
ui/src/app/widget/lib/map-widget2.js

@ -18,6 +18,7 @@ import tinycolor from 'tinycolor2';
import TbGoogleMap from './google-map';
import TbOpenStreetMap from './openstreet-map';
import TbImageMap from './image-map';
import TbTencentMap from './tencent-map';
import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils';
@ -83,6 +84,8 @@ export default class TbMapWidgetV2 {
settings.posFunction,
settings.imageEntityAlias,
settings.imageUrlAttribute);
} else if (mapProvider === 'tencent-map') {
this.map = new TbTencentMap($element,this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
}
}
@ -466,6 +469,8 @@ export default class TbMapWidgetV2 {
schema = angular.copy(openstreetMapSettingsSchema);
} else if (mapProvider === 'image-map') {
return imageMapSettingsSchema;
} else if (mapProvider === 'tencent-map') {
schema = angular.copy(tencentMapSettingsSchema);
}
angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
@ -544,7 +549,51 @@ const googleMapSettingsSchema =
}
]
};
const tencentMapSettingsSchema =
{
"schema":{
"title":"Tencent Map Configuration",
"type":"object",
"properties":{
"tmApiKey":{
"title":"Tencent Maps API Key",
"type":"string"
},
"tmDefaultMapType":{
"title":"Default map type",
"type":"string",
"default":"roadmap"
}
},
"required":[
"tmApiKey"
]
},
"form":[
"tmApiKey",
{
"key":"tmDefaultMapType",
"type":"rc-select",
"multiple":false,
"items":[
{
"value":"roadmap",
"label":"Roadmap"
},
{
"value":"satellite",
"label":"Satellite"
},
{
"value":"hybrid",
"label":"Hybrid"
},
]
}
]
};
const openstreetMapSettingsSchema =
{
"schema":{

391
ui/src/app/widget/lib/tencent-map.js

@ -0,0 +1,391 @@
/*
* Copyright © 2016-2017 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.
*/
var tmGlobals = {
loadingTmId: null,
tmApiKeys: {}
}
export default class TbTencentMap {
constructor($containerElement,utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, tmApiKey, tmDefaultMapType) {
var tbMap = this;
this.utils = utils;
this.defaultZoomLevel = defaultZoomLevel;
this.dontFitMapBounds = dontFitMapBounds;
this.minZoomLevel = minZoomLevel;
this.tooltips = [];
this.defaultMapType = tmDefaultMapType;
function clearGlobalId() {
if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
tmGlobals.loadingTmId = null;
}
}
function displayError(message) {
$containerElement.html( // eslint-disable-line angular/angularelement
"<div class='error'>"+ message + "</div>"
);
}
function initTencentMap() {
tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef
scrollwheel: true,
mapTypeId: getTencentMapTypeId(tbMap.defaultMapType),
zoom: tbMap.defaultZoomLevel || 8
});
if (initCallback) {
initCallback();
}
}
/* eslint-disable no-undef */
function getTencentMapTypeId(mapType) {
var mapTypeId =qq.maps.MapTypeId.ROADMAP;
if (mapType) {
if (mapType === 'hybrid') {
mapTypeId = qq.maps.MapTypeId.HYBRID;
} else if (mapType === 'satellite') {
mapTypeId = qq.maps.MapTypeId.SATELLITE;
} else if (mapType === 'terrain') {
mapTypeId = qq.maps.MapTypeId.ROADMAP;
}
}
return mapTypeId;
}
/* eslint-enable no-undef */
this.mapId = '' + Math.random().toString(36).substr(2, 9);
this.apiKey = tmApiKey || '84d6d83e0e51e481e50454ccbe8986b';
window.tm_authFailure = function() { // eslint-disable-line no-undef, angular/window-service
if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
tmGlobals.loadingTmId = null;
tmGlobals.tmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for tencent Map API.</br>Please check your API key.';
displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
}
};
this.initMapFunctionName = 'initTencentMap_' + this.mapId;
window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service
tmGlobals.tmApiKeys[tbMap.apiKey].loaded = true;
initTencentMap();
for (var p = 0; p < tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits.length; p++) {
var pendingInit = tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits[p];
pendingInit();
}
tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits = [];
};
if (this.apiKey && this.apiKey.length > 0) {
if (tmGlobals.tmApiKeys[this.apiKey]) {
if (tmGlobals.tmApiKeys[this.apiKey].error) {
displayError(tmGlobals.tmApiKeys[this.apiKey].error);
} else if (tmGlobals.tmApiKeys[this.apiKey].loaded) {
initTencentMap();
} else {
tmGlobals.tmApiKeys[this.apiKey].pendingInits.push(initTencentMap);
}
} else {
tmGlobals.tmApiKeys[this.apiKey] = {
loaded: false,
pendingInits: []
};
var tencentMapScriptRes = 'https://map.qq.com/api/js?v=2.exp&key='+this.apiKey+'&callback='+this.initMapFunctionName;
tmGlobals.loadingTmId = this.mapId;
lazyLoad.load({ type: 'js', path: tencentMapScriptRes }).then( // eslint-disable-line no-undef
function success() {
setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service
},
function fail(e) {
clearGlobalId();
tmGlobals.tmApiKeys[tbMap.apiKey].error = 'tencent map api load failed!</br>'+e;
displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
}
);
}
} else {
displayError('No tencent Map Api Key provided!');
}
}
inited() {
return angular.isDefined(this.map);
}
createMarkerLabelStyle(settings) {
return {
width: "200px",
textAlign: "center",
color: settings.labelColor,
background: "none",
border: "none",
fontSize: "12px",
fontFamily: "\"Helvetica Neue\", Arial, Helvetica, sans-serif",
fontWeight: "bold"
};
}
/* eslint-disable no-undef,no-unused-vars*/
updateMarkerLabel(marker, settings) {
if (marker.label) {
marker.label.setContent(settings.labelText);
marker.label.setStyle(this.createMarkerLabelStyle(settings));
}
}
/* eslint-enable no-undef,no-unused-vars */
/* eslint-disable no-undef,no-unused-vars */
updateMarkerColor(marker, color) {
this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
marker.setIcon(iconInfo.icon);
});
}
/* eslint-enable no-undef,,no-unused-vars */
/* eslint-disable no-undef */
updateMarkerIcon(marker, settings) {
this.createMarkerIcon(marker, settings, (iconInfo) => {
marker.setIcon(iconInfo.icon);
if (marker.label) {
marker.label.setOffset(new qq.maps.Size(-100, -iconInfo.size[1]-20));
}
});
}
/* eslint-disable no-undef */
/* eslint-disable no-undef */
createMarkerIcon(marker, settings, onMarkerIconReady) {
var currentImage = settings.currentImage;
var tMap = this;
if (currentImage && currentImage.url) {
this.utils.loadImageAspect(currentImage.url).then(
(aspect) => {
if (aspect) {
var width;
var height;
if (aspect > 1) {
width = currentImage.size;
height = currentImage.size / aspect;
} else {
width = currentImage.size * aspect;
height = currentImage.size;
}
var icon = new qq.maps.MarkerImage(currentImage.url,
new qq.maps.Size(width, height),
new qq.maps.Point(0,0),
new qq.maps.Point(width/2, height),
new qq.maps.Size(width, height));
var iconInfo = {
size: [width, height],
icon: icon
};
onMarkerIconReady(iconInfo);
} else {
tMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
}
}
);
} else {
this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
}
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
var pinColor = color.substr(1);
var icon = new qq.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor,
new qq.maps.Size(40, 37),
new qq.maps.Point(0,0),
new qq.maps.Point(10, 37));
var iconInfo = {
size: [40, 37],
icon: icon
};
onMarkerIconReady(iconInfo);
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
createMarker(location, settings, onClickListener, markerArgs) {
var marker = new qq.maps.Marker({
position: location
});
var tMap = this;
this.createMarkerIcon(marker, settings, (iconInfo) => {
marker.setIcon(iconInfo.icon);
marker.setMap(tMap.map);
if (settings.showLabel) {
marker.label = new qq.maps.Label({
clickable: false,
content: settings.labelText,
offset: new qq.maps.Size(-100, -iconInfo.size[1]-20),
style: tMap.createMarkerLabelStyle(settings),
visible: true,
position: location,
map: tMap.map,
zIndex: 1000
});
}
});
if (settings.displayTooltip) {
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo, settings.autocloseTooltip, markerArgs);
}
if (onClickListener) {
qq.maps.event.addListener(marker, 'click', onClickListener);
}
return marker;
}
/* eslint-disable no-undef */
removeMarker(marker) {
marker.setMap(null);
if (marker.label) {
marker.label.setMap(null);
}
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
createTooltip(marker, pattern, replaceInfo, autoClose, markerArgs) {
var popup = new qq.maps.InfoWindow({
map :this.map
});
var map = this;
qq.maps.event.addListener(marker, 'click', function() {
if (autoClose) {
map.tooltips.forEach((tooltip) => {
tooltip.popup.close();
});
}
popup.open();
popup.setPosition(marker);
});
this.tooltips.push( {
markerArgs: markerArgs,
popup: popup,
pattern: pattern,
replaceInfo: replaceInfo
});
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
updatePolylineColor(polyline, settings, color) {
var options = {
path: polyline.getPath(),
strokeColor: color,
strokeOpacity: settings.strokeOpacity,
strokeWeight: settings.strokeWeight,
map: this.map
};
polyline.setOptions(options);
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
createPolyline(locations, settings) {
var polyline = new qq.maps.Polyline({
path: locations,
strokeColor: settings.color,
strokeOpacity: settings.strokeOpacity,
strokeWeight: settings.strokeWeight,
map: this.map
});
return polyline;
}
/* eslint-enable no-undef */
removePolyline(polyline) {
polyline.setMap(null);
}
/* eslint-disable no-undef ,no-unused-vars*/
fitBounds(bounds) {
if (this.dontFitMapBounds && this.defaultZoomLevel) {
this.map.setZoom(this.defaultZoomLevel);
this.map.setCenter(bounds.getCenter());
} else {
var tbMap = this;
qq.maps.event.addListenerOnce(this.map, 'bounds_changed', function() { // eslint-disable-line no-undef
if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
tbMap.map.setZoom(tbMap.minZoomLevel);
}
});
this.map.fitBounds(bounds);
}
}
/* eslint-enable no-undef,no-unused-vars */
createLatLng(lat, lng) {
return new qq.maps.LatLng(lat, lng); // eslint-disable-line no-undef
}
extendBoundsWithMarker(bounds, marker) {
bounds.extend(marker.getPosition());
}
getMarkerPosition(marker) {
return marker.getPosition();
}
setMarkerPosition(marker, latLng) {
marker.setPosition(latLng);
if (marker.label) {
marker.label.setPosition(latLng);
}
}
getPolylineLatLngs(polyline) {
return polyline.getPath().getArray();
}
setPolylineLatLngs(polyline, latLngs) {
polyline.setPath(latLngs);
}
createBounds() {
return new qq.maps.LatLngBounds(); // eslint-disable-line no-undef
}
extendBounds(bounds, polyline) {
if (polyline && polyline.getPath()) {
var locations = polyline.getPath();
for (var i = 0; i < locations.getLength(); i++) {
bounds.extend(locations.getAt(i));
}
}
}
invalidateSize() {
qq.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef
}
getTooltips() {
return this.tooltips;
}
}

22
ui/src/app/widget/lib/timeseries-table-widget.tpl.html

@ -41,7 +41,7 @@
<md-tabs flex md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}"
id="tabs" md-border-bottom flex>
<md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}">
<md-table-container>
<md-table-container class="flex">
<table md-table>
<thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)">
<tr md-row>
@ -70,16 +70,20 @@
</tr>
</tbody>
</table>
<md-divider></md-divider>
<span ng-show="!vm.sources[vm.sourceIndex].data.length"
layout-align="center center"
class="no-data-found" translate>widget.no-data-found</span>
</md-table-container>
<md-table-pagination ng-if="vm.displayPagination"
md-limit="source.query.limit"
md-limit-options="vm.limitOptions"
md-page="source.query.page"
md-total="{{source.data.length}}"
md-on-paginate="vm.onPaginate(source)"
md-page-select>
</md-table-pagination>
</md-tab>
</md-tabs>
<md-table-pagination ng-if="vm.displayPagination"
md-limit="vm.sources[vm.sourceIndex].query.limit"
md-limit-options="vm.limitOptions"
md-page="vm.sources[vm.sourceIndex].query.page"
md-total="{{vm.sources[vm.sourceIndex].data.length}}"
md-on-paginate="vm.onPaginate(vm.sources[vm.sourceIndex])"
md-page-select>
</md-table-pagination>
</div>
</div>
Loading…
Cancel
Save