Commit 1227b704d25ca203b57717785fcec2f0086b140d

Authored by Artem Halushko
2 parents e7cf3be6 ee07ddb8

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

1 -/**  
2 - * Copyright © 2016-2020 The Thingsboard Authors  
3 - *  
4 - * Licensed under the Apache License, Version 2.0 (the "License");  
5 - * you may not use this file except in compliance with the License.  
6 - * You may obtain a copy of the License at  
7 - *  
8 - * http://www.apache.org/licenses/LICENSE-2.0  
9 - *  
10 - * Unless required by applicable law or agreed to in writing, software  
11 - * distributed under the License is distributed on an "AS IS" BASIS,  
12 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
13 - * See the License for the specific language governing permissions and  
14 - * limitations under the License.  
15 - */  
16 -package org.thingsboard.server.service.install;  
17 -  
18 -import com.datastax.driver.core.KeyspaceMetadata;  
19 -import com.datastax.driver.core.exceptions.InvalidQueryException;  
20 -import lombok.extern.slf4j.Slf4j;  
21 -import org.springframework.beans.factory.annotation.Autowired;  
22 -import org.springframework.context.annotation.Profile;  
23 -import org.springframework.stereotype.Service;  
24 -import org.thingsboard.server.dao.dashboard.DashboardService;  
25 -import org.thingsboard.server.dao.util.NoSqlDao;  
26 -import org.thingsboard.server.service.install.cql.CassandraDbHelper;  
27 -  
28 -import java.nio.file.Files;  
29 -import java.nio.file.Path;  
30 -import java.nio.file.Paths;  
31 -  
32 -import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;  
33 -import static org.thingsboard.server.service.install.DatabaseHelper.ASSET;  
34 -import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;  
35 -import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;  
36 -import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;  
37 -import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;  
38 -import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE;  
39 -import static org.thingsboard.server.service.install.DatabaseHelper.END_TS;  
40 -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_ID;  
41 -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_TYPE;  
42 -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEW;  
43 -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEWS;  
44 -import static org.thingsboard.server.service.install.DatabaseHelper.ID;  
45 -import static org.thingsboard.server.service.install.DatabaseHelper.KEYS;  
46 -import static org.thingsboard.server.service.install.DatabaseHelper.NAME;  
47 -import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;  
48 -import static org.thingsboard.server.service.install.DatabaseHelper.START_TS;  
49 -import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;  
50 -import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;  
51 -import static org.thingsboard.server.service.install.DatabaseHelper.TYPE;  
52 -  
53 -@Service  
54 -@NoSqlDao  
55 -@Profile("install")  
56 -@Slf4j  
57 -public class CassandraDatabaseUpgradeService extends AbstractCassandraDatabaseUpgradeService implements DatabaseEntitiesUpgradeService {  
58 -  
59 - private static final String SCHEMA_UPDATE_CQL = "schema_update.cql";  
60 -  
61 - @Autowired  
62 - private DashboardService dashboardService;  
63 -  
64 - @Autowired  
65 - private InstallScripts installScripts;  
66 -  
67 - @Override  
68 - public void upgradeDatabase(String fromVersion) throws Exception {  
69 -  
70 - switch (fromVersion) {  
71 - case "1.2.3":  
72 -  
73 - log.info("Upgrading Cassandara DataBase from version {} to 1.3.0 ...", fromVersion);  
74 -  
75 - //Dump devices, assets and relations  
76 -  
77 - cluster.getSession();  
78 -  
79 - KeyspaceMetadata ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());  
80 -  
81 - log.info("Dumping devices ...");  
82 - Path devicesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DEVICE,  
83 - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"},  
84 - new String[]{"", "", "", "", "", "", "default"},  
85 - "tb-devices");  
86 - log.info("Devices dumped.");  
87 -  
88 - log.info("Dumping assets ...");  
89 - Path assetsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ASSET,  
90 - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"},  
91 - new String[]{"", "", "", "", "", "", "default"},  
92 - "tb-assets");  
93 - log.info("Assets dumped.");  
94 -  
95 - log.info("Dumping relations ...");  
96 - Path relationsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), "relation",  
97 - new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", ADDITIONAL_INFO, "relation_type_group"},  
98 - new String[]{"", "", "", "", "", "", "COMMON"},  
99 - "tb-relations");  
100 - log.info("Relations dumped.");  
101 -  
102 - log.info("Updating schema ...");  
103 - Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);  
104 - loadCql(schemaUpdateFile);  
105 - log.info("Schema updated.");  
106 -  
107 - //Restore devices, assets and relations  
108 -  
109 - log.info("Restoring devices ...");  
110 - if (devicesDump != null) {  
111 - CassandraDbHelper.loadCf(ks, cluster.getSession(), DEVICE,  
112 - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, devicesDump);  
113 - Files.deleteIfExists(devicesDump);  
114 - }  
115 - log.info("Devices restored.");  
116 -  
117 - log.info("Dumping device types ...");  
118 - Path deviceTypesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DEVICE,  
119 - new String[]{TENANT_ID, "type"},  
120 - new String[]{"", ""},  
121 - "tb-device-types");  
122 - if (deviceTypesDump != null) {  
123 - CassandraDbHelper.appendToEndOfLine(deviceTypesDump, "DEVICE");  
124 - }  
125 - log.info("Device types dumped.");  
126 - log.info("Loading device types ...");  
127 - if (deviceTypesDump != null) {  
128 - CassandraDbHelper.loadCf(ks, cluster.getSession(), "entity_subtype",  
129 - new String[]{TENANT_ID, "type", "entity_type"}, deviceTypesDump);  
130 - Files.deleteIfExists(deviceTypesDump);  
131 - }  
132 - log.info("Device types loaded.");  
133 -  
134 - log.info("Restoring assets ...");  
135 - if (assetsDump != null) {  
136 - CassandraDbHelper.loadCf(ks, cluster.getSession(), ASSET,  
137 - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, assetsDump);  
138 - Files.deleteIfExists(assetsDump);  
139 - }  
140 - log.info("Assets restored.");  
141 -  
142 - log.info("Dumping asset types ...");  
143 - Path assetTypesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ASSET,  
144 - new String[]{TENANT_ID, "type"},  
145 - new String[]{"", ""},  
146 - "tb-asset-types");  
147 - if (assetTypesDump != null) {  
148 - CassandraDbHelper.appendToEndOfLine(assetTypesDump, "ASSET");  
149 - }  
150 - log.info("Asset types dumped.");  
151 - log.info("Loading asset types ...");  
152 - if (assetTypesDump != null) {  
153 - CassandraDbHelper.loadCf(ks, cluster.getSession(), "entity_subtype",  
154 - new String[]{TENANT_ID, "type", "entity_type"}, assetTypesDump);  
155 - Files.deleteIfExists(assetTypesDump);  
156 - }  
157 - log.info("Asset types loaded.");  
158 -  
159 - log.info("Restoring relations ...");  
160 - if (relationsDump != null) {  
161 - CassandraDbHelper.loadCf(ks, cluster.getSession(), "relation",  
162 - new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", ADDITIONAL_INFO, "relation_type_group"}, relationsDump);  
163 - Files.deleteIfExists(relationsDump);  
164 - }  
165 - log.info("Relations restored.");  
166 -  
167 - break;  
168 - case "1.3.0":  
169 - break;  
170 - case "1.3.1":  
171 -  
172 - cluster.getSession();  
173 -  
174 - ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());  
175 -  
176 - log.info("Dumping dashboards ...");  
177 - Path dashboardsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DASHBOARD,  
178 - new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION},  
179 - new String[]{"", "", "", "", "", "", ""},  
180 - "tb-dashboards", true);  
181 - log.info("Dashboards dumped.");  
182 -  
183 -  
184 - log.info("Updating schema ...");  
185 - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);  
186 - loadCql(schemaUpdateFile);  
187 - log.info("Schema updated.");  
188 -  
189 - log.info("Restoring dashboards ...");  
190 - if (dashboardsDump != null) {  
191 - CassandraDbHelper.loadCf(ks, cluster.getSession(), DASHBOARD,  
192 - new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump, true);  
193 - DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, false);  
194 - Files.deleteIfExists(dashboardsDump);  
195 - }  
196 - log.info("Dashboards restored.");  
197 - break;  
198 - case "1.4.0":  
199 -  
200 - log.info("Updating schema ...");  
201 - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_CQL);  
202 - loadCql(schemaUpdateFile);  
203 - log.info("Schema updated.");  
204 -  
205 - break;  
206 -  
207 - case "2.0.0":  
208 -  
209 - log.info("Updating schema ...");  
210 - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.1", SCHEMA_UPDATE_CQL);  
211 - loadCql(schemaUpdateFile);  
212 - log.info("Schema updated.");  
213 -  
214 - break;  
215 -  
216 - case "2.1.1":  
217 -  
218 - log.info("Upgrading Cassandra DataBase from version {} to 2.1.2 ...", fromVersion);  
219 -  
220 - cluster.getSession();  
221 -  
222 - ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());  
223 -  
224 - log.info("Dumping entity views ...");  
225 - Path entityViewsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ENTITY_VIEWS,  
226 - new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO},  
227 - new String[]{"", "", "", "", "", "", "default", "", "0", "0", "", ""},  
228 - "tb-entity-views");  
229 - log.info("Entity views dumped.");  
230 -  
231 - log.info("Updating schema ...");  
232 - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.2", SCHEMA_UPDATE_CQL);  
233 - loadCql(schemaUpdateFile);  
234 - log.info("Schema updated.");  
235 -  
236 - log.info("Restoring entity views ...");  
237 - if (entityViewsDump != null) {  
238 - CassandraDbHelper.loadCf(ks, cluster.getSession(), ENTITY_VIEW,  
239 - new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO}, entityViewsDump);  
240 - Files.deleteIfExists(entityViewsDump);  
241 - }  
242 - log.info("Entity views restored.");  
243 -  
244 - break;  
245 - case "2.1.3":  
246 - break;  
247 - case "2.3.0":  
248 - break;  
249 - case "2.3.1":  
250 - log.info("Updating schema ...");  
251 - String updateDeviceTableStmt = "alter table device add label text";  
252 - try {  
253 - cluster.getSession().execute(updateDeviceTableStmt);  
254 - Thread.sleep(2500);  
255 - } catch (InvalidQueryException e) {  
256 - }  
257 - log.info("Schema updated.");  
258 - break;  
259 - case "2.4.1":  
260 - log.info("Updating schema ...");  
261 - String updateAssetTableStmt = "alter table asset add label text";  
262 - try {  
263 - log.info("Updating assets ...");  
264 - cluster.getSession().execute(updateAssetTableStmt);  
265 - Thread.sleep(2500);  
266 - log.info("Assets updated.");  
267 - } catch (InvalidQueryException e) {  
268 - }  
269 - log.info("Schema updated.");  
270 - break;  
271 - case "2.4.2":  
272 - log.info("Updating schema ...");  
273 - String updateAlarmTableStmt = "alter table alarm add propagate_relation_types text";  
274 - try {  
275 - log.info("Updating alarms ...");  
276 - cluster.getSession().execute(updateAlarmTableStmt);  
277 - Thread.sleep(2500);  
278 - log.info("Alarms updated.");  
279 - } catch (InvalidQueryException e) {  
280 - }  
281 - log.info("Schema updated.");  
282 - break;  
283 - case "2.4.3":  
284 - log.info("Updating schema ...");  
285 - String updateAttributeKvTableStmt = "alter table attributes_kv_cf add json_v text";  
286 - try {  
287 - log.info("Updating attributes ...");  
288 - cluster.getSession().execute(updateAttributeKvTableStmt);  
289 - Thread.sleep(2500);  
290 - log.info("Attributes updated.");  
291 - } catch (InvalidQueryException e) {  
292 - }  
293 - log.info("Schema updated.");  
294 - break;  
295 - default:  
296 - throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);  
297 - }  
298 - }  
299 -  
300 -}  
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
1 package org.thingsboard.server.service.install.migrate; 16 package org.thingsboard.server.service.install.migrate;
2 17
3 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
@@ -6,7 +21,9 @@ import org.springframework.beans.factory.annotation.Value; @@ -6,7 +21,9 @@ import org.springframework.beans.factory.annotation.Value;
6 import org.springframework.context.annotation.Profile; 21 import org.springframework.context.annotation.Profile;
7 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
8 import org.thingsboard.server.common.data.EntityType; 23 import org.thingsboard.server.common.data.EntityType;
  24 +import org.thingsboard.server.common.data.UUIDConverter;
9 import org.thingsboard.server.dao.cassandra.CassandraCluster; 25 import org.thingsboard.server.dao.cassandra.CassandraCluster;
  26 +import org.thingsboard.server.dao.util.NoSqlAnyDao;
10 import org.thingsboard.server.dao.util.SqlDao; 27 import org.thingsboard.server.dao.util.SqlDao;
11 import org.thingsboard.server.service.install.EntityDatabaseSchemaService; 28 import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
12 29
@@ -20,11 +37,13 @@ import static org.thingsboard.server.service.install.migrate.CassandraToSqlColum @@ -20,11 +37,13 @@ import static org.thingsboard.server.service.install.migrate.CassandraToSqlColum
20 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn; 37 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn;
21 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn; 38 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn;
22 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn; 39 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn;
  40 +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.jsonColumn;
23 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn; 41 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn;
24 42
25 @Service 43 @Service
26 @Profile("install") 44 @Profile("install")
27 @SqlDao 45 @SqlDao
  46 +@NoSqlAnyDao
28 @Slf4j 47 @Slf4j
29 public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService { 48 public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService {
30 49
@@ -49,7 +68,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -49,7 +68,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
49 log.info("Performing migration of entities data from cassandra to SQL database ..."); 68 log.info("Performing migration of entities data from cassandra to SQL database ...");
50 entityDatabaseSchemaService.createDatabaseSchema(); 69 entityDatabaseSchemaService.createDatabaseSchema();
51 try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { 70 try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
52 - conn.setAutoCommit(true); 71 + conn.setAutoCommit(false);
53 for (CassandraToSqlTable table: tables) { 72 for (CassandraToSqlTable table: tables) {
54 table.migrateToSql(cluster.getSession(), conn); 73 table.migrateToSql(cluster.getSession(), conn);
55 } 74 }
@@ -60,7 +79,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -60,7 +79,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
60 } 79 }
61 80
62 private static List<CassandraToSqlTable> tables = Arrays.asList( 81 private static List<CassandraToSqlTable> tables = Arrays.asList(
63 - new CassandraToSqlTable("admin_settings", 82 + new CassandraToSqlTable("admin_settings",
64 idColumn("id"), 83 idColumn("id"),
65 stringColumn("key"), 84 stringColumn("key"),
66 stringColumn("json_value")), 85 stringColumn("json_value")),
@@ -87,7 +106,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -87,7 +106,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
87 stringColumn("type"), 106 stringColumn("type"),
88 stringColumn("label"), 107 stringColumn("label"),
89 stringColumn("search_text"), 108 stringColumn("search_text"),
90 - stringColumn("additional_info")), 109 + stringColumn("additional_info")) {
  110 + @Override
  111 + protected boolean onConstraintViolation(List<CassandraToSqlColumnData[]> batchData,
  112 + CassandraToSqlColumnData[] data, String constraint) {
  113 + if (constraint.equalsIgnoreCase("asset_name_unq_key")) {
  114 + this.handleUniqueNameViolation(data, "asset");
  115 + return true;
  116 + }
  117 + return super.onConstraintViolation(batchData, data, constraint);
  118 + }
  119 + },
91 new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log", 120 new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log",
92 idColumn("id"), 121 idColumn("id"),
93 idColumn("tenant_id"), 122 idColumn("tenant_id"),
@@ -110,7 +139,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -110,7 +139,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
110 stringColumn("str_v"), 139 stringColumn("str_v"),
111 bigintColumn("long_v"), 140 bigintColumn("long_v"),
112 doubleColumn("dbl_v"), 141 doubleColumn("dbl_v"),
113 - stringColumn("json_v"), 142 + jsonColumn("json_v"),
114 bigintColumn("last_update_ts")), 143 bigintColumn("last_update_ts")),
115 new CassandraToSqlTable("component_descriptor", 144 new CassandraToSqlTable("component_descriptor",
116 idColumn("id"), 145 idColumn("id"),
@@ -150,7 +179,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -150,7 +179,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
150 stringColumn("type"), 179 stringColumn("type"),
151 stringColumn("label"), 180 stringColumn("label"),
152 stringColumn("search_text"), 181 stringColumn("search_text"),
153 - stringColumn("additional_info")), 182 + stringColumn("additional_info")) {
  183 + @Override
  184 + protected boolean onConstraintViolation(List<CassandraToSqlColumnData[]> batchData,
  185 + CassandraToSqlColumnData[] data, String constraint) {
  186 + if (constraint.equalsIgnoreCase("device_name_unq_key")) {
  187 + this.handleUniqueNameViolation(data, "device");
  188 + return true;
  189 + }
  190 + return super.onConstraintViolation(batchData, data, constraint);
  191 + }
  192 + },
154 new CassandraToSqlTable("device_credentials", 193 new CassandraToSqlTable("device_credentials",
155 idColumn("id"), 194 idColumn("id"),
156 idColumn("device_id"), 195 idColumn("device_id"),
@@ -182,7 +221,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -182,7 +221,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
182 stringColumn("authority"), 221 stringColumn("authority"),
183 stringColumn("first_name"), 222 stringColumn("first_name"),
184 stringColumn("last_name"), 223 stringColumn("last_name"),
185 - stringColumn("additional_info")), 224 + stringColumn("additional_info")) {
  225 + @Override
  226 + protected boolean onConstraintViolation(List<CassandraToSqlColumnData[]> batchData,
  227 + CassandraToSqlColumnData[] data, String constraint) {
  228 + if (constraint.equalsIgnoreCase("tb_user_email_key")) {
  229 + this.handleUniqueEmailViolation(data);
  230 + return true;
  231 + }
  232 + return super.onConstraintViolation(batchData, data, constraint);
  233 + }
  234 + },
186 new CassandraToSqlTable("tenant", 235 new CassandraToSqlTable("tenant",
187 idColumn("id"), 236 idColumn("id"),
188 stringColumn("title"), 237 stringColumn("title"),
@@ -203,7 +252,19 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ @@ -203,7 +252,19 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
203 booleanColumn("enabled"), 252 booleanColumn("enabled"),
204 stringColumn("password"), 253 stringColumn("password"),
205 stringColumn("activate_token"), 254 stringColumn("activate_token"),
206 - stringColumn("reset_token")), 255 + stringColumn("reset_token")) {
  256 + @Override
  257 + protected boolean onConstraintViolation(List<CassandraToSqlColumnData[]> batchData,
  258 + CassandraToSqlColumnData[] data, String constraint) {
  259 + if (constraint.equalsIgnoreCase("user_credentials_user_id_key")) {
  260 + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString();
  261 + log.warn("Found user credentials record with duplicate user_id [id:[{}]]. Record will be ignored!", id);
  262 + this.ignoreRecord(batchData, data);
  263 + return true;
  264 + }
  265 + return super.onConstraintViolation(batchData, data, constraint);
  266 + }
  267 + },
207 new CassandraToSqlTable("widget_type", 268 new CassandraToSqlTable("widget_type",
208 idColumn("id"), 269 idColumn("id"),
209 idColumn("tenant_id"), 270 idColumn("tenant_id"),
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
1 package org.thingsboard.server.service.install.migrate; 16 package org.thingsboard.server.service.install.migrate;
2 17
3 import com.datastax.driver.core.Row; 18 import com.datastax.driver.core.Row;
@@ -7,14 +22,21 @@ import org.thingsboard.server.common.data.UUIDConverter; @@ -7,14 +22,21 @@ import org.thingsboard.server.common.data.UUIDConverter;
7 import java.sql.PreparedStatement; 22 import java.sql.PreparedStatement;
8 import java.sql.SQLException; 23 import java.sql.SQLException;
9 import java.sql.Types; 24 import java.sql.Types;
  25 +import java.util.regex.Pattern;
10 26
11 @Data 27 @Data
12 public class CassandraToSqlColumn { 28 public class CassandraToSqlColumn {
13 29
  30 + private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
  31 + private static final String EMPTY_STR = "";
  32 +
  33 + private int index;
  34 + private int sqlIndex;
14 private String cassandraColumnName; 35 private String cassandraColumnName;
15 private String sqlColumnName; 36 private String sqlColumnName;
16 private CassandraToSqlColumnType type; 37 private CassandraToSqlColumnType type;
17 private int sqlType; 38 private int sqlType;
  39 + private int size;
18 private Class<? extends Enum> enumClass; 40 private Class<? extends Enum> enumClass;
19 41
20 public static CassandraToSqlColumn idColumn(String name) { 42 public static CassandraToSqlColumn idColumn(String name) {
@@ -41,6 +63,10 @@ public class CassandraToSqlColumn { @@ -41,6 +63,10 @@ public class CassandraToSqlColumn {
41 return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BOOLEAN); 63 return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BOOLEAN);
42 } 64 }
43 65
  66 + public static CassandraToSqlColumn jsonColumn(String name) {
  67 + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.JSON);
  68 + }
  69 +
44 public static CassandraToSqlColumn enumToIntColumn(String name, Class<? extends Enum> enumClass) { 70 public static CassandraToSqlColumn enumToIntColumn(String name, Class<? extends Enum> enumClass) {
45 return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass); 71 return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass);
46 } 72 }
@@ -67,36 +93,9 @@ public class CassandraToSqlColumn { @@ -67,36 +93,9 @@ public class CassandraToSqlColumn {
67 this.sqlColumnName = sqlColumnName; 93 this.sqlColumnName = sqlColumnName;
68 this.type = type; 94 this.type = type;
69 this.enumClass = enumClass; 95 this.enumClass = enumClass;
70 - switch (this.type) {  
71 - case ID:  
72 - case STRING:  
73 - this.sqlType = Types.VARCHAR;  
74 - break;  
75 - case DOUBLE:  
76 - this.sqlType = Types.DOUBLE;  
77 - break;  
78 - case INTEGER:  
79 - case ENUM_TO_INT:  
80 - this.sqlType = Types.INTEGER;  
81 - break;  
82 - case FLOAT:  
83 - this.sqlType = Types.FLOAT;  
84 - break;  
85 - case BIGINT:  
86 - this.sqlType = Types.BIGINT;  
87 - break;  
88 - case BOOLEAN:  
89 - this.sqlType = Types.BOOLEAN;  
90 - break;  
91 - }  
92 } 96 }
93 97
94 - public void prepareColumnValue(Row row, PreparedStatement sqlInsertStatement, int index) throws SQLException {  
95 - String value = this.getColumnValue(row, index);  
96 - this.setColumnValue(sqlInsertStatement, index, value);  
97 - }  
98 -  
99 - private String getColumnValue(Row row, int index) { 98 + public String getColumnValue(Row row) {
100 if (row.isNull(index)) { 99 if (row.isNull(index)) {
101 return null; 100 return null;
102 } else { 101 } else {
@@ -114,46 +113,56 @@ public class CassandraToSqlColumn { @@ -114,46 +113,56 @@ public class CassandraToSqlColumn {
114 case BOOLEAN: 113 case BOOLEAN:
115 return Boolean.toString(row.getBool(index)); 114 return Boolean.toString(row.getBool(index));
116 case STRING: 115 case STRING:
  116 + case JSON:
117 case ENUM_TO_INT: 117 case ENUM_TO_INT:
118 default: 118 default:
119 - return row.getString(index); 119 + String value = row.getString(index);
  120 + return this.replaceNullChars(value);
120 } 121 }
121 } 122 }
122 } 123 }
123 124
124 - private void setColumnValue(PreparedStatement sqlInsertStatement, int index, String value) throws SQLException { 125 + public void setColumnValue(PreparedStatement sqlInsertStatement, String value) throws SQLException {
125 if (value == null) { 126 if (value == null) {
126 - sqlInsertStatement.setNull(index, this.sqlType); 127 + sqlInsertStatement.setNull(this.sqlIndex, this.sqlType);
127 } else { 128 } else {
128 switch (this.type) { 129 switch (this.type) {
129 case DOUBLE: 130 case DOUBLE:
130 - sqlInsertStatement.setDouble(index, Double.parseDouble(value)); 131 + sqlInsertStatement.setDouble(this.sqlIndex, Double.parseDouble(value));
131 break; 132 break;
132 case INTEGER: 133 case INTEGER:
133 - sqlInsertStatement.setInt(index, Integer.parseInt(value)); 134 + sqlInsertStatement.setInt(this.sqlIndex, Integer.parseInt(value));
134 break; 135 break;
135 case FLOAT: 136 case FLOAT:
136 - sqlInsertStatement.setFloat(index, Float.parseFloat(value)); 137 + sqlInsertStatement.setFloat(this.sqlIndex, Float.parseFloat(value));
137 break; 138 break;
138 case BIGINT: 139 case BIGINT:
139 - sqlInsertStatement.setLong(index, Long.parseLong(value)); 140 + sqlInsertStatement.setLong(this.sqlIndex, Long.parseLong(value));
140 break; 141 break;
141 case BOOLEAN: 142 case BOOLEAN:
142 - sqlInsertStatement.setBoolean(index, Boolean.parseBoolean(value)); 143 + sqlInsertStatement.setBoolean(this.sqlIndex, Boolean.parseBoolean(value));
143 break; 144 break;
144 case ENUM_TO_INT: 145 case ENUM_TO_INT:
145 Enum enumVal = Enum.valueOf(this.enumClass, value); 146 Enum enumVal = Enum.valueOf(this.enumClass, value);
146 int intValue = enumVal.ordinal(); 147 int intValue = enumVal.ordinal();
147 - sqlInsertStatement.setInt(index, intValue); 148 + sqlInsertStatement.setInt(this.sqlIndex, intValue);
148 break; 149 break;
  150 + case JSON:
149 case STRING: 151 case STRING:
150 case ID: 152 case ID:
151 default: 153 default:
152 - sqlInsertStatement.setString(index, value); 154 + sqlInsertStatement.setString(this.sqlIndex, value);
153 break; 155 break;
154 } 156 }
155 } 157 }
156 } 158 }
157 159
  160 + private String replaceNullChars(String strValue) {
  161 + if (strValue != null) {
  162 + return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR);
  163 + }
  164 + return strValue;
  165 + }
  166 +
158 } 167 }
159 168
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.install.migrate;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class CassandraToSqlColumnData {
  22 +
  23 + private String value;
  24 + private String originalValue;
  25 + private int constraintCounter = 0;
  26 +
  27 + public CassandraToSqlColumnData(String value) {
  28 + this.value = value;
  29 + this.originalValue = value;
  30 + }
  31 +
  32 + public int nextContraintCounter() {
  33 + return ++constraintCounter;
  34 + }
  35 +
  36 + public String getNextConstraintStringValue(CassandraToSqlColumn column) {
  37 + int counter = this.nextContraintCounter();
  38 + String newValue = this.originalValue + counter;
  39 + int overflow = newValue.length() - column.getSize();
  40 + if (overflow > 0) {
  41 + newValue = this.originalValue.substring(0, this.originalValue.length()-overflow) + counter;
  42 + }
  43 + return newValue;
  44 + }
  45 +
  46 + public String getNextConstraintEmailValue(CassandraToSqlColumn column) {
  47 + int counter = this.nextContraintCounter();
  48 + String[] emailValues = this.originalValue.split("@");
  49 + String newValue = emailValues[0] + "+" + counter + "@" + emailValues[1];
  50 + int overflow = newValue.length() - column.getSize();
  51 + if (overflow > 0) {
  52 + newValue = emailValues[0].substring(0, emailValues[0].length()-overflow) + "+" + counter + "@" + emailValues[1];
  53 + }
  54 + return newValue;
  55 + }
  56 +
  57 + public String getLogValue() {
  58 + if (this.value != null && this.value.length() > 255) {
  59 + return this.value.substring(0, 255) + "...[truncated " + (this.value.length() - 255) + " symbols]";
  60 + }
  61 + return this.value;
  62 + }
  63 +
  64 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
1 package org.thingsboard.server.service.install.migrate; 16 package org.thingsboard.server.service.install.migrate;
2 17
3 public enum CassandraToSqlColumnType { 18 public enum CassandraToSqlColumnType {
@@ -8,5 +23,6 @@ public enum CassandraToSqlColumnType { @@ -8,5 +23,6 @@ public enum CassandraToSqlColumnType {
8 BIGINT, 23 BIGINT,
9 BOOLEAN, 24 BOOLEAN,
10 STRING, 25 STRING,
  26 + JSON,
11 ENUM_TO_INT 27 ENUM_TO_INT
12 } 28 }
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
1 package org.thingsboard.server.service.install.migrate; 16 package org.thingsboard.server.service.install.migrate;
2 17
3 import com.datastax.driver.core.ResultSet; 18 import com.datastax.driver.core.ResultSet;
@@ -7,66 +22,255 @@ import com.datastax.driver.core.SimpleStatement; @@ -7,66 +22,255 @@ import com.datastax.driver.core.SimpleStatement;
7 import com.datastax.driver.core.Statement; 22 import com.datastax.driver.core.Statement;
8 import lombok.Data; 23 import lombok.Data;
9 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
  25 +import org.hibernate.exception.ConstraintViolationException;
  26 +import org.hibernate.internal.util.JdbcExceptionHelper;
  27 +import org.postgresql.util.PSQLException;
  28 +import org.thingsboard.server.common.data.UUIDConverter;
  29 +import org.thingsboard.server.dao.exception.DataValidationException;
10 30
  31 +import java.sql.BatchUpdateException;
11 import java.sql.Connection; 32 import java.sql.Connection;
  33 +import java.sql.DatabaseMetaData;
12 import java.sql.PreparedStatement; 34 import java.sql.PreparedStatement;
13 import java.sql.SQLException; 35 import java.sql.SQLException;
  36 +import java.util.ArrayList;
14 import java.util.Arrays; 37 import java.util.Arrays;
15 import java.util.Iterator; 38 import java.util.Iterator;
16 import java.util.List; 39 import java.util.List;
  40 +import java.util.Optional;
  41 +import java.util.stream.Collectors;
17 42
18 @Data 43 @Data
19 @Slf4j 44 @Slf4j
20 public class CassandraToSqlTable { 45 public class CassandraToSqlTable {
21 46
  47 + private static final int DEFAULT_BATCH_SIZE = 10000;
  48 +
22 private String cassandraCf; 49 private String cassandraCf;
23 private String sqlTableName; 50 private String sqlTableName;
24 51
25 private List<CassandraToSqlColumn> columns; 52 private List<CassandraToSqlColumn> columns;
26 53
  54 + private int batchSize = DEFAULT_BATCH_SIZE;
  55 +
  56 + private PreparedStatement sqlInsertStatement;
  57 +
27 public CassandraToSqlTable(String tableName, CassandraToSqlColumn... columns) { 58 public CassandraToSqlTable(String tableName, CassandraToSqlColumn... columns) {
28 - this(tableName, tableName, columns); 59 + this(tableName, tableName, DEFAULT_BATCH_SIZE, columns);
  60 + }
  61 +
  62 + public CassandraToSqlTable(String tableName, String sqlTableName, CassandraToSqlColumn... columns) {
  63 + this(tableName, sqlTableName, DEFAULT_BATCH_SIZE, columns);
  64 + }
  65 +
  66 + public CassandraToSqlTable(String tableName, int batchSize, CassandraToSqlColumn... columns) {
  67 + this(tableName, tableName, batchSize, columns);
29 } 68 }
30 69
31 - public CassandraToSqlTable(String cassandraCf, String sqlTableName, CassandraToSqlColumn... columns) { 70 + public CassandraToSqlTable(String cassandraCf, String sqlTableName, int batchSize, CassandraToSqlColumn... columns) {
32 this.cassandraCf = cassandraCf; 71 this.cassandraCf = cassandraCf;
33 this.sqlTableName = sqlTableName; 72 this.sqlTableName = sqlTableName;
  73 + this.batchSize = batchSize;
34 this.columns = Arrays.asList(columns); 74 this.columns = Arrays.asList(columns);
  75 + for (int i=0;i<columns.length;i++) {
  76 + this.columns.get(i).setIndex(i);
  77 + this.columns.get(i).setSqlIndex(i+1);
  78 + }
35 } 79 }
36 80
37 public void migrateToSql(Session session, Connection conn) throws SQLException { 81 public void migrateToSql(Session session, Connection conn) throws SQLException {
38 - log.info("Migrating data from cassandra '{}' Column Family to '{}' SQL table...", this.cassandraCf, this.sqlTableName);  
39 - PreparedStatement sqlInsertStatement = createSqlInsertStatement(conn); 82 + log.info("[{}] Migrating data from cassandra '{}' Column Family to '{}' SQL table...", this.sqlTableName, this.cassandraCf, this.sqlTableName);
  83 + DatabaseMetaData metadata = conn.getMetaData();
  84 + java.sql.ResultSet resultSet = metadata.getColumns(null, null, this.sqlTableName, null);
  85 + while (resultSet.next()) {
  86 + String name = resultSet.getString("COLUMN_NAME");
  87 + int sqlType = resultSet.getInt("DATA_TYPE");
  88 + int size = resultSet.getInt("COLUMN_SIZE");
  89 + CassandraToSqlColumn column = this.getColumn(name);
  90 + column.setSize(size);
  91 + column.setSqlType(sqlType);
  92 + }
  93 + this.sqlInsertStatement = createSqlInsertStatement(conn);
40 Statement cassandraSelectStatement = createCassandraSelectStatement(); 94 Statement cassandraSelectStatement = createCassandraSelectStatement();
41 cassandraSelectStatement.setFetchSize(100); 95 cassandraSelectStatement.setFetchSize(100);
42 ResultSet rs = session.execute(cassandraSelectStatement); 96 ResultSet rs = session.execute(cassandraSelectStatement);
43 Iterator<Row> iter = rs.iterator(); 97 Iterator<Row> iter = rs.iterator();
44 int rowCounter = 0; 98 int rowCounter = 0;
45 - while (iter.hasNext()) { 99 + List<CassandraToSqlColumnData[]> batchData;
  100 + boolean hasNext;
  101 + do {
  102 + batchData = this.extractBatchData(iter);
  103 + hasNext = batchData.size() == this.batchSize;
  104 + this.batchInsert(batchData, conn);
  105 + rowCounter += batchData.size();
  106 + log.info("[{}] {} records migrated so far...", this.sqlTableName, rowCounter);
  107 + } while (hasNext);
  108 + this.sqlInsertStatement.close();
  109 + log.info("[{}] {} total records migrated.", this.sqlTableName, rowCounter);
  110 + log.info("[{}] Finished migration data from cassandra '{}' Column Family to '{}' SQL table.",
  111 + this.sqlTableName, this.cassandraCf, this.sqlTableName);
  112 + }
  113 +
  114 + private List<CassandraToSqlColumnData[]> extractBatchData(Iterator<Row> iter) {
  115 + List<CassandraToSqlColumnData[]> batchData = new ArrayList<>();
  116 + while (iter.hasNext() && batchData.size() < this.batchSize) {
46 Row row = iter.next(); 117 Row row = iter.next();
47 if (row != null) { 118 if (row != null) {
48 - this.migrateRowToSql(row, sqlInsertStatement);  
49 - rowCounter++;  
50 - if (rowCounter % 100 == 0) {  
51 - sqlInsertStatement.executeBatch();  
52 - log.info("{} records migrated so far...", rowCounter);  
53 - } 119 + CassandraToSqlColumnData[] data = this.extractRowData(row);
  120 + batchData.add(data);
54 } 121 }
55 } 122 }
56 - if (rowCounter % 100 > 0) {  
57 - sqlInsertStatement.executeBatch(); 123 + return batchData;
  124 + }
  125 +
  126 + private CassandraToSqlColumnData[] extractRowData(Row row) {
  127 + CassandraToSqlColumnData[] data = new CassandraToSqlColumnData[this.columns.size()];
  128 + for (CassandraToSqlColumn column: this.columns) {
  129 + String value = column.getColumnValue(row);
  130 + data[column.getIndex()] = new CassandraToSqlColumnData(value);
58 } 131 }
59 - sqlInsertStatement.close();  
60 - log.info("{} total records migrated.", rowCounter);  
61 - log.info("Finished migration data from cassandra '{}' Column Family to '{}' SQL table.", this.cassandraCf, this.sqlTableName); 132 + return this.validateColumnData(data);
62 } 133 }
63 134
64 - private void migrateRowToSql(Row row, PreparedStatement sqlInsertStatement) throws SQLException {  
65 - for (int i=0; i<this.columns.size();i++) { 135 + private CassandraToSqlColumnData[] validateColumnData(CassandraToSqlColumnData[] data) {
  136 + for (int i=0;i<data.length;i++) {
66 CassandraToSqlColumn column = this.columns.get(i); 137 CassandraToSqlColumn column = this.columns.get(i);
67 - column.prepareColumnValue(row, sqlInsertStatement, i); 138 + if (column.getType() == CassandraToSqlColumnType.STRING) {
  139 + CassandraToSqlColumnData columnData = data[i];
  140 + String value = columnData.getValue();
  141 + if (value != null && value.length() > column.getSize()) {
  142 + log.warn("[{}] Value size [{}] exceeds maximum size [{}] of column [{}] and will be truncated!",
  143 + this.sqlTableName,
  144 + value.length(), column.getSize(), column.getSqlColumnName());
  145 + log.warn("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data));
  146 + value = value.substring(0, column.getSize());
  147 + columnData.setOriginalValue(value);
  148 + columnData.setValue(value);
  149 + }
  150 + }
  151 + }
  152 + return data;
  153 + }
  154 +
  155 + private void batchInsert(List<CassandraToSqlColumnData[]> batchData, Connection conn) throws SQLException {
  156 + boolean retry = false;
  157 + for (CassandraToSqlColumnData[] data : batchData) {
  158 + for (CassandraToSqlColumn column: this.columns) {
  159 + column.setColumnValue(this.sqlInsertStatement, data[column.getIndex()].getValue());
  160 + }
  161 + try {
  162 + this.sqlInsertStatement.executeUpdate();
  163 + } catch (SQLException e) {
  164 + if (this.handleInsertException(batchData, data, conn, e)) {
  165 + retry = true;
  166 + break;
  167 + } else {
  168 + throw e;
  169 + }
  170 + }
  171 + }
  172 + if (retry) {
  173 + this.batchInsert(batchData, conn);
  174 + } else {
  175 + conn.commit();
  176 + }
  177 + }
  178 +
  179 + private boolean handleInsertException(List<CassandraToSqlColumnData[]> batchData,
  180 + CassandraToSqlColumnData[] data,
  181 + Connection conn, SQLException ex) throws SQLException {
  182 + conn.commit();
  183 + String constraint = extractConstraintName(ex).orElse(null);
  184 + if (constraint != null) {
  185 + if (this.onConstraintViolation(batchData, data, constraint)) {
  186 + return true;
  187 + } else {
  188 + log.error("[{}] Unhandled constraint violation [{}] during insert!", this.sqlTableName, constraint);
  189 + log.error("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data));
  190 + }
  191 + } else {
  192 + log.error("[{}] Unhandled exception during insert!", this.sqlTableName);
  193 + log.error("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data));
68 } 194 }
69 - sqlInsertStatement.addBatch(); 195 + return false;
  196 + }
  197 +
  198 + private String dataToString(CassandraToSqlColumnData[] data) {
  199 + StringBuffer stringData = new StringBuffer("{\n");
  200 + for (int i=0;i<data.length;i++) {
  201 + String columnName = this.columns.get(i).getSqlColumnName();
  202 + String value = data[i].getLogValue();
  203 + stringData.append("\"").append(columnName).append("\": ").append("[").append(value).append("]\n");
  204 + }
  205 + stringData.append("}");
  206 + return stringData.toString();
  207 + }
  208 +
  209 + protected boolean onConstraintViolation(List<CassandraToSqlColumnData[]> batchData,
  210 + CassandraToSqlColumnData[] data, String constraint) {
  211 + return false;
  212 + }
  213 +
  214 + protected void handleUniqueNameViolation(CassandraToSqlColumnData[] data, String entityType) {
  215 + CassandraToSqlColumn nameColumn = this.getColumn("name");
  216 + CassandraToSqlColumn searchTextColumn = this.getColumn("search_text");
  217 + CassandraToSqlColumnData nameColumnData = data[nameColumn.getIndex()];
  218 + CassandraToSqlColumnData searchTextColumnData = data[searchTextColumn.getIndex()];
  219 + String prevName = nameColumnData.getValue();
  220 + String newName = nameColumnData.getNextConstraintStringValue(nameColumn);
  221 + nameColumnData.setValue(newName);
  222 + searchTextColumnData.setValue(searchTextColumnData.getNextConstraintStringValue(searchTextColumn));
  223 + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString();
  224 + log.warn("Found {} with duplicate name [id:[{}]]. Attempting to rename {} from '{}' to '{}'...", entityType, id, entityType, prevName, newName);
  225 + }
  226 +
  227 + protected void handleUniqueEmailViolation(CassandraToSqlColumnData[] data) {
  228 + CassandraToSqlColumn emailColumn = this.getColumn("email");
  229 + CassandraToSqlColumn searchTextColumn = this.getColumn("search_text");
  230 + CassandraToSqlColumnData emailColumnData = data[emailColumn.getIndex()];
  231 + CassandraToSqlColumnData searchTextColumnData = data[searchTextColumn.getIndex()];
  232 + String prevEmail = emailColumnData.getValue();
  233 + String newEmail = emailColumnData.getNextConstraintEmailValue(emailColumn);
  234 + emailColumnData.setValue(newEmail);
  235 + searchTextColumnData.setValue(searchTextColumnData.getNextConstraintEmailValue(searchTextColumn));
  236 + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString();
  237 + log.warn("Found user with duplicate email [id:[{}]]. Attempting to rename email from '{}' to '{}'...", id, prevEmail, newEmail);
  238 + }
  239 +
  240 + protected void ignoreRecord(List<CassandraToSqlColumnData[]> batchData, CassandraToSqlColumnData[] data) {
  241 + log.warn("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data));
  242 + int index = batchData.indexOf(data);
  243 + if (index > 0) {
  244 + batchData.remove(index);
  245 + }
  246 + }
  247 +
  248 + protected CassandraToSqlColumn getColumn(String sqlColumnName) {
  249 + return this.columns.stream().filter(col -> col.getSqlColumnName().equals(sqlColumnName)).findFirst().get();
  250 + }
  251 +
  252 + protected CassandraToSqlColumnData getColumnData(CassandraToSqlColumnData[] data, String sqlColumnName) {
  253 + CassandraToSqlColumn column = this.getColumn(sqlColumnName);
  254 + return data[column.getIndex()];
  255 + }
  256 +
  257 + private Optional<String> extractConstraintName(SQLException ex) {
  258 + final String sqlState = JdbcExceptionHelper.extractSqlState( ex );
  259 + if (sqlState != null) {
  260 + String sqlStateClassCode = JdbcExceptionHelper.determineSqlStateClassCode( sqlState );
  261 + if ( sqlStateClassCode != null ) {
  262 + if (Arrays.asList(
  263 + "23", // "integrity constraint violation"
  264 + "27", // "triggered data change violation"
  265 + "44" // "with check option violation"
  266 + ).contains(sqlStateClassCode)) {
  267 + if (ex instanceof PSQLException) {
  268 + return Optional.of(((PSQLException)ex).getServerErrorMessage().getConstraint());
  269 + }
  270 + }
  271 + }
  272 + }
  273 + return Optional.empty();
70 } 274 }
71 275
72 private Statement createCassandraSelectStatement() { 276 private Statement createCassandraSelectStatement() {
@@ -88,8 +292,13 @@ public class CassandraToSqlTable { @@ -88,8 +292,13 @@ public class CassandraToSqlTable {
88 } 292 }
89 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); 293 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
90 insertStatementBuilder.append(") VALUES ("); 294 insertStatementBuilder.append(") VALUES (");
91 - for (CassandraToSqlColumn ignored : columns) {  
92 - insertStatementBuilder.append("?").append(","); 295 + for (CassandraToSqlColumn column : columns) {
  296 + if (column.getType() == CassandraToSqlColumnType.JSON) {
  297 + insertStatementBuilder.append("cast(? AS json)");
  298 + } else {
  299 + insertStatementBuilder.append("?");
  300 + }
  301 + insertStatementBuilder.append(",");
93 } 302 }
94 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); 303 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
95 insertStatementBuilder.append(")"); 304 insertStatementBuilder.append(")");
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
1 package org.thingsboard.server.service.install.migrate; 16 package org.thingsboard.server.service.install.migrate;
2 17
3 public interface EntitiesMigrateService { 18 public interface EntitiesMigrateService {
@@ -20,7 +20,7 @@ import BaseGauge = CanvasGauges.BaseGauge; @@ -20,7 +20,7 @@ import BaseGauge = CanvasGauges.BaseGauge;
20 import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; 20 import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models';
21 import * as tinycolor_ from 'tinycolor2'; 21 import * as tinycolor_ from 'tinycolor2';
22 import { ColorFormats } from 'tinycolor2'; 22 import { ColorFormats } from 'tinycolor2';
23 -import { isDefined, isUndefined } from '@core/utils'; 23 +import { isDefined, isString, isUndefined } from '@core/utils';
24 24
25 const tinycolor = tinycolor_; 25 const tinycolor = tinycolor_;
26 26
@@ -32,13 +32,20 @@ export interface DigitalGaugeColorRange { @@ -32,13 +32,20 @@ export interface DigitalGaugeColorRange {
32 rgbString: string; 32 rgbString: string;
33 } 33 }
34 34
  35 +export interface colorLevelSetting {
  36 + value: number;
  37 + color: string;
  38 +}
  39 +
  40 +export type levelColors = Array<string | colorLevelSetting>;
  41 +
35 export interface CanvasDigitalGaugeOptions extends GenericOptions { 42 export interface CanvasDigitalGaugeOptions extends GenericOptions {
36 gaugeType?: GaugeType; 43 gaugeType?: GaugeType;
37 gaugeWithScale?: number; 44 gaugeWithScale?: number;
38 dashThickness?: number; 45 dashThickness?: number;
39 roundedLineCap?: boolean; 46 roundedLineCap?: boolean;
40 gaugeColor?: string; 47 gaugeColor?: string;
41 - levelColors?: string[]; 48 + levelColors?: levelColors;
42 symbol?: string; 49 symbol?: string;
43 label?: string; 50 label?: string;
44 hideValue?: boolean; 51 hideValue?: boolean;
@@ -229,26 +236,30 @@ export class CanvasDigitalGauge extends BaseGauge { @@ -229,26 +236,30 @@ export class CanvasDigitalGauge extends BaseGauge {
229 } 236 }
230 237
231 const colorsCount = options.levelColors.length; 238 const colorsCount = options.levelColors.length;
  239 + const isColorProperty = isString(options.levelColors[0]);
232 const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; 240 const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
233 options.colorsRange = []; 241 options.colorsRange = [];
234 if (options.neonGlowBrightness) { 242 if (options.neonGlowBrightness) {
235 options.neonColorsRange = []; 243 options.neonColorsRange = [];
236 } 244 }
237 for (let i = 0; i < options.levelColors.length; i++) { 245 for (let i = 0; i < options.levelColors.length; i++) {
238 - const percentage = inc * i;  
239 - let tColor = tinycolor(options.levelColors[i]);  
240 - options.colorsRange[i] = {  
241 - pct: percentage,  
242 - color: tColor.toRgb(),  
243 - rgbString: tColor.toRgbString()  
244 - };  
245 - if (options.neonGlowBrightness) {  
246 - tColor = tinycolor(options.levelColors[i]).brighten(options.neonGlowBrightness);  
247 - options.neonColorsRange[i] = { 246 + let levelColor: any = options.levelColors[i];
  247 + if (levelColor !== null) {
  248 + let percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
  249 + let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color);
  250 + options.colorsRange.push({
248 pct: percentage, 251 pct: percentage,
249 color: tColor.toRgb(), 252 color: tColor.toRgb(),
250 rgbString: tColor.toRgbString() 253 rgbString: tColor.toRgbString()
251 - }; 254 + });
  255 + if (options.neonGlowBrightness) {
  256 + tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness);
  257 + options.neonColorsRange.push({
  258 + pct: percentage,
  259 + color: tColor.toRgb(),
  260 + rgbString: tColor.toRgbString()
  261 + });
  262 + }
252 } 263 }
253 } 264 }
254 265
@@ -262,6 +273,17 @@ export class CanvasDigitalGauge extends BaseGauge { @@ -262,6 +273,17 @@ export class CanvasDigitalGauge extends BaseGauge {
262 return options; 273 return options;
263 } 274 }
264 275
  276 + static normalizeValue (value: number, min: number, max: number): number {
  277 + let normalValue = (value - min) / (max - min);
  278 + if (normalValue <= 0) {
  279 + return 0;
  280 + }
  281 + if (normalValue >= 1) {
  282 + return 1;
  283 + }
  284 + return normalValue;
  285 + }
  286 +
265 private initValueClone() { 287 private initValueClone() {
266 const canvas = this.canvas; 288 const canvas = this.canvas;
267 this.elementValueClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone; 289 this.elementValueClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone;
@@ -19,6 +19,26 @@ import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; @@ -19,6 +19,26 @@ import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge';
19 import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models'; 19 import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models';
20 import { FontSettings } from '@home/components/widget/lib/settings.models'; 20 import { FontSettings } from '@home/components/widget/lib/settings.models';
21 21
  22 +export interface colorLevelProperty {
  23 + valueSource: string;
  24 + entityAlias?: string;
  25 + attribute?: string;
  26 + value?: number;
  27 +}
  28 +
  29 +export interface fixedLevelColors {
  30 + from?: colorLevelProperty;
  31 + to?: colorLevelProperty;
  32 + color: string;
  33 +}
  34 +
  35 +export interface colorLevelSetting {
  36 + value: number;
  37 + color: string;
  38 +}
  39 +
  40 +export type colorLevel = Array<string | colorLevelSetting>;
  41 +
22 export interface DigitalGaugeSettings { 42 export interface DigitalGaugeSettings {
23 minValue?: number; 43 minValue?: number;
24 maxValue?: number; 44 maxValue?: number;
@@ -38,7 +58,9 @@ export interface DigitalGaugeSettings { @@ -38,7 +58,9 @@ export interface DigitalGaugeSettings {
38 gaugeWidthScale?: number; 58 gaugeWidthScale?: number;
39 defaultColor?: string; 59 defaultColor?: string;
40 gaugeColor?: string; 60 gaugeColor?: string;
41 - levelColors?: string[]; 61 + useFixedLevelColor?: boolean;
  62 + levelColors?: colorLevel;
  63 + fixedLevelColors?: fixedLevelColors[];
42 animation?: boolean; 64 animation?: boolean;
43 animationDuration?: number; 65 animationDuration?: number;
44 animationRule?: AnimationRule; 66 animationRule?: AnimationRule;
@@ -147,6 +169,11 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { @@ -147,6 +169,11 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
147 type: 'string', 169 type: 'string',
148 default: null 170 default: null
149 }, 171 },
  172 + useFixedLevelColor: {
  173 + title: 'Use precise value for the color indicator',
  174 + type: 'boolean',
  175 + default: false
  176 + },
150 levelColors: { 177 levelColors: {
151 title: 'Colors of indicator, from lower to upper', 178 title: 'Colors of indicator, from lower to upper',
152 type: 'array', 179 type: 'array',
@@ -155,6 +182,66 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { @@ -155,6 +182,66 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
155 type: 'string' 182 type: 'string'
156 } 183 }
157 }, 184 },
  185 + fixedLevelColors: {
  186 + title: 'The colors for the indicator using boundary values',
  187 + type: 'array',
  188 + items: {
  189 + title: 'levelColor',
  190 + type: 'object',
  191 + properties: {
  192 + from: {
  193 + title: 'From',
  194 + type: 'object',
  195 + properties: {
  196 + valueSource: {
  197 + title: '[From] Value source',
  198 + type: 'string',
  199 + default: 'predefinedValue'
  200 + },
  201 + entityAlias: {
  202 + title: '[From] Source entity alias',
  203 + type: 'string'
  204 + },
  205 + attribute: {
  206 + title: '[From] Source entity attribute',
  207 + type: 'string'
  208 + },
  209 + value: {
  210 + title: '[From] Value (if predefined value is selected)',
  211 + type: 'number'
  212 + }
  213 + }
  214 + },
  215 + to: {
  216 + title: 'To',
  217 + type: 'object',
  218 + properties: {
  219 + valueSource: {
  220 + title: '[To] Value source',
  221 + type: 'string',
  222 + default: 'predefinedValue'
  223 + },
  224 + entityAlias: {
  225 + title: '[To] Source entity alias',
  226 + type: 'string'
  227 + },
  228 + attribute: {
  229 + title: '[To] Source entity attribute',
  230 + type: 'string'
  231 + },
  232 + value: {
  233 + title: '[To] Value (if predefined value is selected)',
  234 + type: 'number'
  235 + }
  236 + }
  237 + },
  238 + color: {
  239 + title: 'Color',
  240 + type: 'string'
  241 + }
  242 + }
  243 + }
  244 + },
158 animation: { 245 animation: {
159 title: 'Enable animation', 246 title: 'Enable animation',
160 type: 'boolean', 247 type: 'boolean',
@@ -343,8 +430,10 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { @@ -343,8 +430,10 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
343 key: 'gaugeColor', 430 key: 'gaugeColor',
344 type: 'color' 431 type: 'color'
345 }, 432 },
  433 + 'useFixedLevelColor',
346 { 434 {
347 key: 'levelColors', 435 key: 'levelColors',
  436 + condition: 'model.useFixedLevelColor !== true',
348 items: [ 437 items: [
349 { 438 {
350 key: 'levelColors[]', 439 key: 'levelColors[]',
@@ -352,6 +441,52 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { @@ -352,6 +441,52 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
352 } 441 }
353 ] 442 ]
354 }, 443 },
  444 + {
  445 + key: 'fixedLevelColors',
  446 + condition: 'model.useFixedLevelColor === true',
  447 + items: [
  448 + {
  449 + key: 'fixedLevelColors[].from.valueSource',
  450 + type: 'rc-select',
  451 + multiple: false,
  452 + items: [
  453 + {
  454 + value: 'predefinedValue',
  455 + label: 'Predefined value (Default)'
  456 + },
  457 + {
  458 + value: 'entityAttribute',
  459 + label: 'Value taken from entity attribute'
  460 + }
  461 + ]
  462 + },
  463 + 'fixedLevelColors[].from.value',
  464 + 'fixedLevelColors[].from.entityAlias',
  465 + 'fixedLevelColors[].from.attribute',
  466 + {
  467 + key: 'fixedLevelColors[].to.valueSource',
  468 + type: 'rc-select',
  469 + multiple: false,
  470 + items: [
  471 + {
  472 + value: 'predefinedValue',
  473 + label: 'Predefined value (Default)'
  474 + },
  475 + {
  476 + value: 'entityAttribute',
  477 + label: 'Value taken from entity attribute'
  478 + }
  479 + ]
  480 + },
  481 + 'fixedLevelColors[].to.value',
  482 + 'fixedLevelColors[].to.entityAlias',
  483 + 'fixedLevelColors[].to.attribute',
  484 + {
  485 + key: 'fixedLevelColors[].color',
  486 + type: 'color'
  487 + }
  488 + ]
  489 + },
355 'animation', 490 'animation',
356 'animationDuration', 491 'animationDuration',
357 { 492 {
@@ -16,14 +16,20 @@ @@ -16,14 +16,20 @@
16 16
17 import * as CanvasGauges from 'canvas-gauges'; 17 import * as CanvasGauges from 'canvas-gauges';
18 import { WidgetContext } from '@home/models/widget-component.models'; 18 import { WidgetContext } from '@home/models/widget-component.models';
19 -import { DigitalGaugeSettings, digitalGaugeSettingsSchema } from '@home/components/widget/lib/digital-gauge.models'; 19 +import {
  20 + colorLevelSetting,
  21 + DigitalGaugeSettings,
  22 + digitalGaugeSettingsSchema
  23 +} from '@home/components/widget/lib/digital-gauge.models';
20 import * as tinycolor_ from 'tinycolor2'; 24 import * as tinycolor_ from 'tinycolor2';
21 import { isDefined } from '@core/utils'; 25 import { isDefined } from '@core/utils';
22 import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; 26 import { prepareFontSettings } from '@home/components/widget/lib/settings.models';
23 import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; 27 import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge';
24 import { DatePipe } from '@angular/common'; 28 import { DatePipe } from '@angular/common';
25 -import { JsonSettingsSchema } from '@shared/models/widget.models'; 29 +import {DataKey, Datasource, DatasourceType, JsonSettingsSchema, widgetType} from '@shared/models/widget.models';
26 import GenericOptions = CanvasGauges.GenericOptions; 30 import GenericOptions = CanvasGauges.GenericOptions;
  31 +import {IWidgetSubscription, WidgetSubscriptionOptions} from "@core/api/widget-api.models";
  32 +import {DataKeyType} from "@shared/models/telemetry/telemetry.models";
27 33
28 const tinycolor = tinycolor_; 34 const tinycolor = tinycolor_;
29 35
@@ -32,6 +38,7 @@ const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema; @@ -32,6 +38,7 @@ const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema;
32 export class TbCanvasDigitalGauge { 38 export class TbCanvasDigitalGauge {
33 39
34 private localSettings: DigitalGaugeSettings; 40 private localSettings: DigitalGaugeSettings;
  41 + private levelColorsSourcesSubscription: IWidgetSubscription;
35 42
36 private gauge: CanvasDigitalGauge; 43 private gauge: CanvasDigitalGauge;
37 44
@@ -65,10 +72,16 @@ export class TbCanvasDigitalGauge { @@ -65,10 +72,16 @@ export class TbCanvasDigitalGauge {
65 this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75; 72 this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
66 this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString(); 73 this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString();
67 74
68 - if (!settings.levelColors || settings.levelColors.length <= 0) {  
69 - this.localSettings.levelColors = [keyColor]; 75 + this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false;
  76 + if (!settings.useFixedLevelColor) {
  77 + if (!settings.levelColors || settings.levelColors.length <= 0) {
  78 + this.localSettings.levelColors = [keyColor];
  79 + } else {
  80 + this.localSettings.levelColors = settings.levelColors.slice();
  81 + }
70 } else { 82 } else {
71 - this.localSettings.levelColors = settings.levelColors.slice(); 83 + this.localSettings.levelColors = [keyColor];
  84 + this.localSettings.fixedLevelColors = settings.fixedLevelColors || [];
72 } 85 }
73 86
74 this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals : 87 this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals :
@@ -176,6 +189,130 @@ export class TbCanvasDigitalGauge { @@ -176,6 +189,130 @@ export class TbCanvasDigitalGauge {
176 }; 189 };
177 190
178 this.gauge = new CanvasDigitalGauge(gaugeData).draw(); 191 this.gauge = new CanvasDigitalGauge(gaugeData).draw();
  192 + this.init();
  193 + }
  194 +
  195 + init() {
  196 + if (this.localSettings.useFixedLevelColor) {
  197 + if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) {
  198 + this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
  199 + this.updateLevelColors(this.localSettings.levelColors);
  200 + }
  201 + }
  202 + }
  203 +
  204 + settingLevelColorsSubscribe(options) {
  205 + let levelColorsDatasource: Datasource[] = [];
  206 + let predefineLevelColors: colorLevelSetting[] = [];
  207 +
  208 + function setLevelColor(levelSetting, color) {
  209 + if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) {
  210 + predefineLevelColors.push({
  211 + value: levelSetting.value,
  212 + color: color
  213 + })
  214 + } else if (levelSetting.entityAlias && levelSetting.attribute) {
  215 + let entityAliasId = this.ctx.aliasController.getEntityAliasId(levelSetting.entityAlias);
  216 + if (!entityAliasId) {
  217 + return;
  218 + }
  219 +
  220 + let datasource = levelColorsDatasource.find((datasource) => {
  221 + return datasource.entityAliasId === entityAliasId;
  222 + });
  223 +
  224 + let dataKey: DataKey = {
  225 + type: DataKeyType.attribute,
  226 + name: levelSetting.attribute,
  227 + label: levelSetting.attribute,
  228 + settings: [{
  229 + color: color,
  230 + index: predefineLevelColors.length
  231 + }],
  232 + _hash: Math.random()
  233 + };
  234 +
  235 + if (datasource) {
  236 + let findDataKey = datasource.dataKeys.find((dataKey) => {
  237 + return dataKey.name === levelSetting.attribute;
  238 + });
  239 +
  240 + if (findDataKey) {
  241 + findDataKey.settings.push({
  242 + color: color,
  243 + index: predefineLevelColors.length
  244 + });
  245 + } else {
  246 + datasource.dataKeys.push(dataKey)
  247 + }
  248 + } else {
  249 + let datasource: Datasource = {
  250 + type: DatasourceType.entity,
  251 + name: levelSetting.entityAlias,
  252 + aliasName: levelSetting.entityAlias,
  253 + entityAliasId: entityAliasId,
  254 + dataKeys: [dataKey]
  255 + };
  256 + levelColorsDatasource.push(datasource);
  257 + }
  258 +
  259 + predefineLevelColors.push(null);
  260 + }
  261 + }
  262 +
  263 + for (let i = 0; i < options.length; i++) {
  264 + let levelColor = options[i];
  265 + if (levelColor.from) {
  266 + setLevelColor.call(this, levelColor.from, levelColor.color);
  267 + }
  268 + if (levelColor.to) {
  269 + setLevelColor.call(this, levelColor.to, levelColor.color);
  270 + }
  271 + }
  272 +
  273 + this.subscribeLevelColorsAttributes(levelColorsDatasource);
  274 +
  275 + return predefineLevelColors;
  276 + }
  277 +
  278 + updateLevelColors(levelColors) {
  279 + (this.gauge.options as CanvasDigitalGaugeOptions).levelColors = levelColors;
  280 + this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options);
  281 + this.gauge.update({} as CanvasDigitalGaugeOptions);
  282 + }
  283 +
  284 + subscribeLevelColorsAttributes(datasources: Datasource[]) {
  285 + let TbCanvasDigitalGauge = this;
  286 + let levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = {
  287 + datasources: datasources,
  288 + useDashboardTimewindow: false,
  289 + type: widgetType.latest,
  290 + callbacks: {
  291 + onDataUpdated: (subscription) => {
  292 + for (let i = 0; i < subscription.data.length; i++) {
  293 + let keyData = subscription.data[i];
  294 + if (keyData && keyData.data && keyData.data[0]) {
  295 + let attrValue = keyData.data[0][1];
  296 + if (isFinite(attrValue)) {
  297 + for (let i = 0; i < keyData.dataKey.settings.length; i++) {
  298 + let setting = keyData.dataKey.settings[i];
  299 + this.localSettings.levelColors[setting.index] = {
  300 + value: attrValue,
  301 + color: setting.color
  302 + };
  303 + }
  304 + }
  305 + }
  306 + }
  307 + this.updateLevelColors(this.localSettings.levelColors);
  308 + }
  309 + }
  310 + };
  311 + this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true).subscribe(
  312 + (subscription) => {
  313 + TbCanvasDigitalGauge.levelColorsSourcesSubscription = subscription;
  314 + }
  315 + );
179 } 316 }
180 317
181 update() { 318 update() {
@@ -37,7 +37,7 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon @@ -37,7 +37,7 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
37 adminSettings: AdminSettings<MailServerSettings>; 37 adminSettings: AdminSettings<MailServerSettings>;
38 smtpProtocols = ['smtp', 'smtps']; 38 smtpProtocols = ['smtp', 'smtps'];
39 39
40 - tlsVersions = ['TLSv1.0', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; 40 + tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
41 41
42 constructor(protected store: Store<AppState>, 42 constructor(protected store: Store<AppState>,
43 private router: Router, 43 private router: Router,
@@ -186,7 +186,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -186,7 +186,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
186 val = undefined; 186 val = undefined;
187 } 187 }
188 if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) { 188 if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) {
189 - this.formProps.model = this.model;  
190 this.isModelValid = this.validateModel(); 189 this.isModelValid = this.validateModel();
191 this.updateView(); 190 this.updateView();
192 } 191 }
@@ -233,7 +232,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -233,7 +232,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
233 this.formProps.schema = this.schema; 232 this.formProps.schema = this.schema;
234 this.formProps.form = this.form; 233 this.formProps.form = this.form;
235 this.formProps.groupInfoes = this.groupInfoes; 234 this.formProps.groupInfoes = this.groupInfoes;
236 - this.formProps.model = deepClone(this.model); 235 + this.formProps.model = this.model;
237 this.renderReactSchemaForm(); 236 this.renderReactSchemaForm();
238 } 237 }
239 238