Commit 1227b704d25ca203b57717785fcec2f0086b140d
Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0
Showing
12 changed files
with
756 additions
and
389 deletions
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
deleted
100644 → 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(")"); | ... | ... |
application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java
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 | ... | ... |