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 16 package org.thingsboard.server.service.install.migrate;
2 17
3 18 import lombok.extern.slf4j.Slf4j;
... ... @@ -6,7 +21,9 @@ import org.springframework.beans.factory.annotation.Value;
6 21 import org.springframework.context.annotation.Profile;
7 22 import org.springframework.stereotype.Service;
8 23 import org.thingsboard.server.common.data.EntityType;
  24 +import org.thingsboard.server.common.data.UUIDConverter;
9 25 import org.thingsboard.server.dao.cassandra.CassandraCluster;
  26 +import org.thingsboard.server.dao.util.NoSqlAnyDao;
10 27 import org.thingsboard.server.dao.util.SqlDao;
11 28 import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
12 29
... ... @@ -20,11 +37,13 @@ import static org.thingsboard.server.service.install.migrate.CassandraToSqlColum
20 37 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn;
21 38 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn;
22 39 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn;
  40 +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.jsonColumn;
23 41 import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn;
24 42
25 43 @Service
26 44 @Profile("install")
27 45 @SqlDao
  46 +@NoSqlAnyDao
28 47 @Slf4j
29 48 public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService {
30 49
... ... @@ -49,7 +68,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
49 68 log.info("Performing migration of entities data from cassandra to SQL database ...");
50 69 entityDatabaseSchemaService.createDatabaseSchema();
51 70 try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
52   - conn.setAutoCommit(true);
  71 + conn.setAutoCommit(false);
53 72 for (CassandraToSqlTable table: tables) {
54 73 table.migrateToSql(cluster.getSession(), conn);
55 74 }
... ... @@ -60,7 +79,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
60 79 }
61 80
62 81 private static List<CassandraToSqlTable> tables = Arrays.asList(
63   - new CassandraToSqlTable("admin_settings",
  82 + new CassandraToSqlTable("admin_settings",
64 83 idColumn("id"),
65 84 stringColumn("key"),
66 85 stringColumn("json_value")),
... ... @@ -87,7 +106,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
87 106 stringColumn("type"),
88 107 stringColumn("label"),
89 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 120 new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log",
92 121 idColumn("id"),
93 122 idColumn("tenant_id"),
... ... @@ -110,7 +139,7 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
110 139 stringColumn("str_v"),
111 140 bigintColumn("long_v"),
112 141 doubleColumn("dbl_v"),
113   - stringColumn("json_v"),
  142 + jsonColumn("json_v"),
114 143 bigintColumn("last_update_ts")),
115 144 new CassandraToSqlTable("component_descriptor",
116 145 idColumn("id"),
... ... @@ -150,7 +179,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
150 179 stringColumn("type"),
151 180 stringColumn("label"),
152 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 193 new CassandraToSqlTable("device_credentials",
155 194 idColumn("id"),
156 195 idColumn("device_id"),
... ... @@ -182,7 +221,17 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
182 221 stringColumn("authority"),
183 222 stringColumn("first_name"),
184 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 235 new CassandraToSqlTable("tenant",
187 236 idColumn("id"),
188 237 stringColumn("title"),
... ... @@ -203,7 +252,19 @@ public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateServ
203 252 booleanColumn("enabled"),
204 253 stringColumn("password"),
205 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 268 new CassandraToSqlTable("widget_type",
208 269 idColumn("id"),
209 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 16 package org.thingsboard.server.service.install.migrate;
2 17
3 18 import com.datastax.driver.core.Row;
... ... @@ -7,14 +22,21 @@ import org.thingsboard.server.common.data.UUIDConverter;
7 22 import java.sql.PreparedStatement;
8 23 import java.sql.SQLException;
9 24 import java.sql.Types;
  25 +import java.util.regex.Pattern;
10 26
11 27 @Data
12 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 35 private String cassandraColumnName;
15 36 private String sqlColumnName;
16 37 private CassandraToSqlColumnType type;
17 38 private int sqlType;
  39 + private int size;
18 40 private Class<? extends Enum> enumClass;
19 41
20 42 public static CassandraToSqlColumn idColumn(String name) {
... ... @@ -41,6 +63,10 @@ public class CassandraToSqlColumn {
41 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 70 public static CassandraToSqlColumn enumToIntColumn(String name, Class<? extends Enum> enumClass) {
45 71 return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass);
46 72 }
... ... @@ -67,36 +93,9 @@ public class CassandraToSqlColumn {
67 93 this.sqlColumnName = sqlColumnName;
68 94 this.type = type;
69 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 99 if (row.isNull(index)) {
101 100 return null;
102 101 } else {
... ... @@ -114,46 +113,56 @@ public class CassandraToSqlColumn {
114 113 case BOOLEAN:
115 114 return Boolean.toString(row.getBool(index));
116 115 case STRING:
  116 + case JSON:
117 117 case ENUM_TO_INT:
118 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 126 if (value == null) {
126   - sqlInsertStatement.setNull(index, this.sqlType);
  127 + sqlInsertStatement.setNull(this.sqlIndex, this.sqlType);
127 128 } else {
128 129 switch (this.type) {
129 130 case DOUBLE:
130   - sqlInsertStatement.setDouble(index, Double.parseDouble(value));
  131 + sqlInsertStatement.setDouble(this.sqlIndex, Double.parseDouble(value));
131 132 break;
132 133 case INTEGER:
133   - sqlInsertStatement.setInt(index, Integer.parseInt(value));
  134 + sqlInsertStatement.setInt(this.sqlIndex, Integer.parseInt(value));
134 135 break;
135 136 case FLOAT:
136   - sqlInsertStatement.setFloat(index, Float.parseFloat(value));
  137 + sqlInsertStatement.setFloat(this.sqlIndex, Float.parseFloat(value));
137 138 break;
138 139 case BIGINT:
139   - sqlInsertStatement.setLong(index, Long.parseLong(value));
  140 + sqlInsertStatement.setLong(this.sqlIndex, Long.parseLong(value));
140 141 break;
141 142 case BOOLEAN:
142   - sqlInsertStatement.setBoolean(index, Boolean.parseBoolean(value));
  143 + sqlInsertStatement.setBoolean(this.sqlIndex, Boolean.parseBoolean(value));
143 144 break;
144 145 case ENUM_TO_INT:
145 146 Enum enumVal = Enum.valueOf(this.enumClass, value);
146 147 int intValue = enumVal.ordinal();
147   - sqlInsertStatement.setInt(index, intValue);
  148 + sqlInsertStatement.setInt(this.sqlIndex, intValue);
148 149 break;
  150 + case JSON:
149 151 case STRING:
150 152 case ID:
151 153 default:
152   - sqlInsertStatement.setString(index, value);
  154 + sqlInsertStatement.setString(this.sqlIndex, value);
153 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 16 package org.thingsboard.server.service.install.migrate;
2 17
3 18 public enum CassandraToSqlColumnType {
... ... @@ -8,5 +23,6 @@ public enum CassandraToSqlColumnType {
8 23 BIGINT,
9 24 BOOLEAN,
10 25 STRING,
  26 + JSON,
11 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 16 package org.thingsboard.server.service.install.migrate;
2 17
3 18 import com.datastax.driver.core.ResultSet;
... ... @@ -7,66 +22,255 @@ import com.datastax.driver.core.SimpleStatement;
7 22 import com.datastax.driver.core.Statement;
8 23 import lombok.Data;
9 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 32 import java.sql.Connection;
  33 +import java.sql.DatabaseMetaData;
12 34 import java.sql.PreparedStatement;
13 35 import java.sql.SQLException;
  36 +import java.util.ArrayList;
14 37 import java.util.Arrays;
15 38 import java.util.Iterator;
16 39 import java.util.List;
  40 +import java.util.Optional;
  41 +import java.util.stream.Collectors;
17 42
18 43 @Data
19 44 @Slf4j
20 45 public class CassandraToSqlTable {
21 46
  47 + private static final int DEFAULT_BATCH_SIZE = 10000;
  48 +
22 49 private String cassandraCf;
23 50 private String sqlTableName;
24 51
25 52 private List<CassandraToSqlColumn> columns;
26 53
  54 + private int batchSize = DEFAULT_BATCH_SIZE;
  55 +
  56 + private PreparedStatement sqlInsertStatement;
  57 +
27 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 71 this.cassandraCf = cassandraCf;
33 72 this.sqlTableName = sqlTableName;
  73 + this.batchSize = batchSize;
34 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 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 94 Statement cassandraSelectStatement = createCassandraSelectStatement();
41 95 cassandraSelectStatement.setFetchSize(100);
42 96 ResultSet rs = session.execute(cassandraSelectStatement);
43 97 Iterator<Row> iter = rs.iterator();
44 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 117 Row row = iter.next();
47 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 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 276 private Statement createCassandraSelectStatement() {
... ... @@ -88,8 +292,13 @@ public class CassandraToSqlTable {
88 292 }
89 293 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
90 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 303 insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
95 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 16 package org.thingsboard.server.service.install.migrate;
2 17
3 18 public interface EntitiesMigrateService {
... ...
... ... @@ -20,7 +20,7 @@ import BaseGauge = CanvasGauges.BaseGauge;
20 20 import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models';
21 21 import * as tinycolor_ from 'tinycolor2';
22 22 import { ColorFormats } from 'tinycolor2';
23   -import { isDefined, isUndefined } from '@core/utils';
  23 +import { isDefined, isString, isUndefined } from '@core/utils';
24 24
25 25 const tinycolor = tinycolor_;
26 26
... ... @@ -32,13 +32,20 @@ export interface DigitalGaugeColorRange {
32 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 42 export interface CanvasDigitalGaugeOptions extends GenericOptions {
36 43 gaugeType?: GaugeType;
37 44 gaugeWithScale?: number;
38 45 dashThickness?: number;
39 46 roundedLineCap?: boolean;
40 47 gaugeColor?: string;
41   - levelColors?: string[];
  48 + levelColors?: levelColors;
42 49 symbol?: string;
43 50 label?: string;
44 51 hideValue?: boolean;
... ... @@ -229,26 +236,30 @@ export class CanvasDigitalGauge extends BaseGauge {
229 236 }
230 237
231 238 const colorsCount = options.levelColors.length;
  239 + const isColorProperty = isString(options.levelColors[0]);
232 240 const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
233 241 options.colorsRange = [];
234 242 if (options.neonGlowBrightness) {
235 243 options.neonColorsRange = [];
236 244 }
237 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 251 pct: percentage,
249 252 color: tColor.toRgb(),
250 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 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 287 private initValueClone() {
266 288 const canvas = this.canvas;
267 289 this.elementValueClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone;
... ...
... ... @@ -19,6 +19,26 @@ import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge';
19 19 import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models';
20 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 42 export interface DigitalGaugeSettings {
23 43 minValue?: number;
24 44 maxValue?: number;
... ... @@ -38,7 +58,9 @@ export interface DigitalGaugeSettings {
38 58 gaugeWidthScale?: number;
39 59 defaultColor?: string;
40 60 gaugeColor?: string;
41   - levelColors?: string[];
  61 + useFixedLevelColor?: boolean;
  62 + levelColors?: colorLevel;
  63 + fixedLevelColors?: fixedLevelColors[];
42 64 animation?: boolean;
43 65 animationDuration?: number;
44 66 animationRule?: AnimationRule;
... ... @@ -147,6 +169,11 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
147 169 type: 'string',
148 170 default: null
149 171 },
  172 + useFixedLevelColor: {
  173 + title: 'Use precise value for the color indicator',
  174 + type: 'boolean',
  175 + default: false
  176 + },
150 177 levelColors: {
151 178 title: 'Colors of indicator, from lower to upper',
152 179 type: 'array',
... ... @@ -155,6 +182,66 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
155 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 245 animation: {
159 246 title: 'Enable animation',
160 247 type: 'boolean',
... ... @@ -343,8 +430,10 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
343 430 key: 'gaugeColor',
344 431 type: 'color'
345 432 },
  433 + 'useFixedLevelColor',
346 434 {
347 435 key: 'levelColors',
  436 + condition: 'model.useFixedLevelColor !== true',
348 437 items: [
349 438 {
350 439 key: 'levelColors[]',
... ... @@ -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 490 'animation',
356 491 'animationDuration',
357 492 {
... ...
... ... @@ -16,14 +16,20 @@
16 16
17 17 import * as CanvasGauges from 'canvas-gauges';
18 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 24 import * as tinycolor_ from 'tinycolor2';
21 25 import { isDefined } from '@core/utils';
22 26 import { prepareFontSettings } from '@home/components/widget/lib/settings.models';
23 27 import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge';
24 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 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 34 const tinycolor = tinycolor_;
29 35
... ... @@ -32,6 +38,7 @@ const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema;
32 38 export class TbCanvasDigitalGauge {
33 39
34 40 private localSettings: DigitalGaugeSettings;
  41 + private levelColorsSourcesSubscription: IWidgetSubscription;
35 42
36 43 private gauge: CanvasDigitalGauge;
37 44
... ... @@ -65,10 +72,16 @@ export class TbCanvasDigitalGauge {
65 72 this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
66 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 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 87 this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals :
... ... @@ -176,6 +189,130 @@ export class TbCanvasDigitalGauge {
176 189 };
177 190
178 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 318 update() {
... ...
... ... @@ -37,7 +37,7 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
37 37 adminSettings: AdminSettings<MailServerSettings>;
38 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 42 constructor(protected store: Store<AppState>,
43 43 private router: Router,
... ...
... ... @@ -186,7 +186,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
186 186 val = undefined;
187 187 }
188 188 if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) {
189   - this.formProps.model = this.model;
190 189 this.isModelValid = this.validateModel();
191 190 this.updateView();
192 191 }
... ... @@ -233,7 +232,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
233 232 this.formProps.schema = this.schema;
234 233 this.formProps.form = this.form;
235 234 this.formProps.groupInfoes = this.groupInfoes;
236   - this.formProps.model = deepClone(this.model);
  235 + this.formProps.model = this.model;
237 236 this.renderReactSchemaForm();
238 237 }
239 238
... ...