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