]> git.basschouten.com Git - openhab-addons.git/blob
1e72aaba629c79a811bb9ad6adaf503aba1e5ad6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.jdbc.internal;
14
15 import java.sql.SQLInvalidAuthorizationSpecException;
16 import java.time.ZonedDateTime;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Set;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.knowm.yank.Yank;
28 import org.openhab.core.i18n.TimeZoneProvider;
29 import org.openhab.core.items.Item;
30 import org.openhab.core.items.ItemUtil;
31 import org.openhab.core.persistence.FilterCriteria;
32 import org.openhab.core.persistence.HistoricItem;
33 import org.openhab.core.persistence.PersistenceItemInfo;
34 import org.openhab.core.types.State;
35 import org.openhab.persistence.jdbc.internal.dto.Column;
36 import org.openhab.persistence.jdbc.internal.dto.ItemVO;
37 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
38 import org.openhab.persistence.jdbc.internal.dto.JdbcPersistenceItemInfo;
39 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
40 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
45
46 /**
47  * Mapper class
48  *
49  * @author Helmut Lehmeyer - Initial contribution
50  */
51 @NonNullByDefault
52 public class JdbcMapper {
53     private static final int MIGRATION_PERCENTAGE_THRESHOLD = 50;
54
55     private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
56     private final TimeZoneProvider timeZoneProvider;
57
58     // Error counter - used to reconnect to database on error
59     protected int errCnt;
60     protected boolean initialized = false;
61     protected @NonNullByDefault({}) JdbcConfiguration conf;
62     protected final Map<String, String> itemNameToTableNameMap = new HashMap<>();
63     protected @NonNullByDefault({}) NamingStrategy namingStrategy;
64     private long afterAccessMin = 10000;
65     private long afterAccessMax = 0;
66
67     public JdbcMapper(TimeZoneProvider timeZoneProvider) {
68         this.timeZoneProvider = timeZoneProvider;
69     }
70
71     /****************
72      * MAPPER ITEMS *
73      ****************/
74     private boolean pingDB() throws JdbcSQLException {
75         logger.debug("JDBC::pingDB");
76         boolean ret = false;
77         long timerStart = System.currentTimeMillis();
78         if (openConnection()) {
79             if (conf.getDbName() == null) {
80                 logger.debug(
81                         "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
82                 String dbName = conf.getDBDAO().doGetDB();
83                 if (dbName == null) {
84                     ret = false;
85                 } else {
86                     conf.setDbName(dbName);
87                     ret = dbName.length() > 0;
88                 }
89             } else {
90                 final @Nullable Integer result = conf.getDBDAO().doPingDB();
91                 ret = result != null && result > 0;
92             }
93         }
94         logTime("pingDB", timerStart, System.currentTimeMillis());
95         return ret;
96     }
97
98     private boolean ifItemsTableExists() throws JdbcSQLException {
99         logger.debug("JDBC::ifItemsTableExists");
100         long timerStart = System.currentTimeMillis();
101         ItemsVO vo = new ItemsVO();
102         vo.setItemsManageTable(conf.getItemsManageTable());
103         boolean res = conf.getDBDAO().doIfTableExists(vo);
104         logTime("doIfTableExists", timerStart, System.currentTimeMillis());
105         return res;
106     }
107
108     protected boolean ifTableExists(String tableName) throws JdbcSQLException {
109         logger.debug("JDBC::ifTableExists");
110         long timerStart = System.currentTimeMillis();
111         boolean res = conf.getDBDAO().doIfTableExists(tableName);
112         logTime("doIfTableExists", timerStart, System.currentTimeMillis());
113         return res;
114     }
115
116     private ItemsVO createNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
117         logger.debug("JDBC::createNewEntryInItemsTable");
118         long timerStart = System.currentTimeMillis();
119         Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
120         vo.setItemId(i.intValue());
121         logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
122         return vo;
123     }
124
125     private boolean createItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
126         logger.debug("JDBC::createItemsTableIfNot");
127         long timerStart = System.currentTimeMillis();
128         conf.getDBDAO().doCreateItemsTableIfNot(vo);
129         logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
130         return true;
131     }
132
133     private boolean dropItemsTableIfExists(ItemsVO vo) throws JdbcSQLException {
134         logger.debug("JDBC::dropItemsTableIfExists");
135         long timerStart = System.currentTimeMillis();
136         conf.getDBDAO().doDropItemsTableIfExists(vo);
137         logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis());
138         return true;
139     }
140
141     protected void dropTable(String tableName) throws JdbcSQLException {
142         logger.debug("JDBC::dropTable");
143         long timerStart = System.currentTimeMillis();
144         conf.getDBDAO().doDropTable(tableName);
145         logTime("doDropTable", timerStart, System.currentTimeMillis());
146     }
147
148     protected ItemsVO deleteItemsEntry(ItemsVO vo) throws JdbcSQLException {
149         logger.debug("JDBC::deleteItemsEntry");
150         long timerStart = System.currentTimeMillis();
151         conf.getDBDAO().doDeleteItemsEntry(vo);
152         logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
153         return vo;
154     }
155
156     private List<ItemsVO> getItemIDTableNames() throws JdbcSQLException {
157         logger.debug("JDBC::getItemIDTableNames");
158         long timerStart = System.currentTimeMillis();
159         ItemsVO isvo = new ItemsVO();
160         isvo.setItemsManageTable(conf.getItemsManageTable());
161         List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(isvo);
162         logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
163         return vo;
164     }
165
166     protected List<ItemsVO> getItemTables() throws JdbcSQLException {
167         logger.debug("JDBC::getItemTables");
168         long timerStart = System.currentTimeMillis();
169         ItemsVO isvo = new ItemsVO();
170         isvo.setJdbcUriDatabaseName(conf.getDbName());
171         isvo.setItemsManageTable(conf.getItemsManageTable());
172         List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(isvo);
173         logTime("getItemTables", timerStart, System.currentTimeMillis());
174         return vol;
175     }
176
177     protected List<Column> getTableColumns(String tableName) throws JdbcSQLException {
178         logger.debug("JDBC::getTableColumns");
179         long timerStart = System.currentTimeMillis();
180         ItemsVO isvo = new ItemsVO();
181         isvo.setJdbcUriDatabaseName(conf.getDbName());
182         isvo.setTableName(tableName);
183         isvo.setItemsManageTable(conf.getItemsManageTable());
184         List<Column> is = conf.getDBDAO().doGetTableColumns(isvo);
185         logTime("getTableColumns", timerStart, System.currentTimeMillis());
186         return is;
187     }
188
189     /****************
190      * MAPPERS ITEM *
191      ****************/
192     private void updateItemTableNames(List<ItemVO> vol) throws JdbcSQLException {
193         logger.debug("JDBC::updateItemTableNames");
194         long timerStart = System.currentTimeMillis();
195         conf.getDBDAO().doUpdateItemTableNames(vol);
196         logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
197     }
198
199     private ItemVO createItemTable(ItemVO vo) throws JdbcSQLException {
200         logger.debug("JDBC::createItemTable");
201         long timerStart = System.currentTimeMillis();
202         conf.getDBDAO().doCreateItemTable(vo);
203         logTime("createItemTable", timerStart, System.currentTimeMillis());
204         return vo;
205     }
206
207     protected void alterTableColumn(String tableName, String columnName, String columnType, boolean nullable)
208             throws JdbcSQLException {
209         logger.debug("JDBC::alterTableColumn");
210         long timerStart = System.currentTimeMillis();
211         conf.getDBDAO().doAlterTableColumn(tableName, columnName, columnType, nullable);
212         logTime("alterTableColumn", timerStart, System.currentTimeMillis());
213     }
214
215     protected void storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) throws JdbcException {
216         logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
217         String tableName = getTable(item);
218         long timerStart = System.currentTimeMillis();
219         if (date == null) {
220             conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
221         } else {
222             conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
223         }
224         logTime("storeItemValue", timerStart, System.currentTimeMillis());
225         errCnt = 0;
226     }
227
228     public long getRowCount(String tableName) throws JdbcSQLException {
229         return conf.getDBDAO().doGetRowCount(tableName);
230     }
231
232     protected List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
233             Item item) throws JdbcSQLException {
234         logger.debug(
235                 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
236                 true, numberDecimalcount, table, item, item.getName());
237         long timerStart = System.currentTimeMillis();
238         List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
239                 item.getName(), timeZoneProvider.getTimeZone());
240         logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
241         errCnt = 0;
242         return result;
243     }
244
245     protected void deleteItemValues(FilterCriteria filter, String table) throws JdbcSQLException {
246         logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
247         long timerStart = System.currentTimeMillis();
248         conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
249         logTime("deleteItemValues", timerStart, System.currentTimeMillis());
250         errCnt = 0;
251     }
252
253     /***********************
254      * DATABASE CONNECTION *
255      ***********************/
256     protected boolean openConnection() {
257         logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
258         if (conf.isDriverAvailable() && !conf.isDbConnected()) {
259             logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
260             try {
261                 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
262                 Yank.setThrowWrappedExceptions(true);
263                 conf.setDbConnected(true);
264                 return true;
265             } catch (PoolInitializationException e) {
266                 Throwable cause = e.getCause();
267                 if (cause instanceof SQLInvalidAuthorizationSpecException) {
268                     logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
269                 } else {
270                     logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
271                 }
272                 initialized = false;
273                 return false;
274             }
275         } else if (!conf.isDriverAvailable()) {
276             logger.warn("JDBC::openConnection: no driver available!");
277             initialized = false;
278             return false;
279         }
280         return true;
281     }
282
283     protected void closeConnection() {
284         logger.debug("JDBC::closeConnection");
285         // Closes all open connection pools
286         Yank.releaseDefaultConnectionPool();
287         conf.setDbConnected(false);
288     }
289
290     protected boolean checkDBAccessability() {
291         // Check if connection is valid
292         if (initialized) {
293             return true;
294         }
295         try {
296             // first
297             boolean p = pingDB();
298             if (p) {
299                 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
300                 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
301             } else {
302                 // second
303                 p = pingDB();
304                 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
305                 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
306             }
307         } catch (JdbcSQLException e) {
308             logger.warn("Unable to ping database", e);
309             return false;
310         }
311     }
312
313     /**************************
314      * DATABASE TABLEHANDLING *
315      **************************/
316     protected void checkDBSchema() throws JdbcSQLException {
317         ItemsVO vo = new ItemsVO();
318         vo.setItemsManageTable(conf.getItemsManageTable());
319
320         if (!conf.getTableUseRealCaseSensitiveItemNames()) {
321             createItemsTableIfNot(vo);
322         }
323         if (conf.getRebuildTableNames()) {
324             formatTableNames();
325
326             if (conf.getTableUseRealCaseSensitiveItemNames()) {
327                 dropItemsTableIfExists(vo);
328             }
329             logger.info(
330                     "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
331             // Reset the error counter
332             errCnt = 0;
333         }
334         populateItemNameToTableNameMap();
335     }
336
337     public void populateItemNameToTableNameMap() throws JdbcSQLException {
338         itemNameToTableNameMap.clear();
339         if (conf.getTableUseRealCaseSensitiveItemNames()) {
340             for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) {
341                 itemNameToTableNameMap.put(itemName, itemName);
342             }
343         } else {
344             for (ItemsVO vo : getItemIDTableNames()) {
345                 String itemName = vo.getItemName();
346                 if (!ItemUtil.isValidItemName(itemName)) {
347                     logger.warn("Skipping invalid item name {}", itemName);
348                     continue;
349                 }
350                 itemNameToTableNameMap.put(vo.getItemName(),
351                         namingStrategy.getTableName(vo.getItemId(), vo.getItemName()));
352             }
353         }
354     }
355
356     protected String getTable(Item item) throws JdbcException {
357         String itemName = item.getName();
358         if (!initialized) {
359             throw new JdbcException("Not initialized, unable to find table for item " + itemName);
360         }
361
362         // Table already exists - return the name
363         String tableName = itemNameToTableNameMap.get(itemName);
364         if (!Objects.isNull(tableName)) {
365             return tableName;
366         }
367
368         logger.debug("JDBC::getTable: no table found for item '{}' in itemNameToTableNameMap", itemName);
369
370         int itemId = 0;
371
372         if (!conf.getTableUseRealCaseSensitiveItemNames()) {
373             // Create a new entry in items table
374             ItemsVO isvo = new ItemsVO();
375             isvo.setItemName(itemName);
376             isvo.setItemsManageTable(conf.getItemsManageTable());
377             isvo = createNewEntryInItemsTable(isvo);
378             itemId = isvo.getItemId();
379             if (itemId == 0) {
380                 logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName);
381             }
382         }
383
384         // Create the table name
385         logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName);
386         tableName = namingStrategy.getTableName(itemId, itemName);
387
388         // Create table for item
389         String dataType = conf.getDBDAO().getDataType(item);
390         ItemVO ivo = new ItemVO(tableName, itemName);
391         ivo.setDbType(dataType);
392         ivo = createItemTable(ivo);
393         logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
394                 dataType);
395
396         itemNameToTableNameMap.put(itemName, tableName);
397
398         return tableName;
399     }
400
401     private void formatTableNames() throws JdbcSQLException {
402         boolean tmpinit = initialized;
403         if (tmpinit) {
404             initialized = false;
405         }
406
407         List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
408         var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList());
409         List<ItemVO> oldNewTableNames;
410
411         if (itemIdTableNames.isEmpty()) {
412             // Without mappings we can only migrate from direct item name to numeric mapping.
413             if (conf.getTableUseRealCaseSensitiveItemNames()) {
414                 logger.info("JDBC::formatTableNames: Nothing to migrate.");
415                 initialized = tmpinit;
416                 return;
417             }
418             // Safety valve to prevent accidental migrations.
419             int numberOfTables = itemTables.size();
420             if (numberOfTables > 0) {
421                 String prefix = conf.getTableNamePrefix();
422                 long numberOfItemsWithPrefix = itemTables.stream()
423                         .filter(i -> i.startsWith(prefix) || i.toLowerCase().startsWith("item")).count();
424                 long percentageWithPrefix = (numberOfItemsWithPrefix * 100) / itemTables.size();
425                 if (!prefix.isBlank() && percentageWithPrefix >= MIGRATION_PERCENTAGE_THRESHOLD) {
426                     logger.error(
427                             "JDBC::formatTableNames: {}% of all tables start with table name prefix '{}' or 'item', but items manage table '{}' was not found or is empty. Check configuration parameter 'itemsManageTable'",
428                             percentageWithPrefix, conf.getTableNamePrefix(), conf.getItemsManageTable());
429                     if (ifTableExists("items")) {
430                         logger.error(
431                                 "JDBC::formatTableNames: Table 'items' was found, consider updating configuration parameter 'itemsManageTable' accordingly");
432                     }
433                     initialized = tmpinit;
434                     return;
435                 }
436             }
437             oldNewTableNames = new ArrayList<>();
438             for (String itemName : itemTables) {
439                 if (!ItemUtil.isValidItemName(itemName)) {
440                     logger.warn("JDBC::formatTableNames: Skipping invalid item name {}", itemName);
441                     continue;
442                 }
443                 ItemsVO isvo = new ItemsVO();
444                 isvo.setItemName(itemName);
445                 isvo.setItemsManageTable(conf.getItemsManageTable());
446                 isvo = createNewEntryInItemsTable(isvo);
447                 int itemId = isvo.getItemId();
448                 if (itemId == 0) {
449                     logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName);
450                 } else {
451                     String newTableName = namingStrategy.getTableName(itemId, itemName);
452                     oldNewTableNames.add(new ItemVO(itemName, newTableName));
453                     logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName);
454                 }
455             }
456         } else {
457             String itemsManageTable = conf.getItemsManageTable();
458             Map<Integer, String> itemIdToItemNameMap = new HashMap<>();
459
460             for (ItemsVO vo : itemIdTableNames) {
461                 int itemId = vo.getItemId();
462                 String itemName = vo.getItemName();
463                 itemIdToItemNameMap.put(itemId, itemName);
464             }
465
466             oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
467         }
468
469         updateItemTableNames(oldNewTableNames);
470         logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
471
472         initialized = tmpinit;
473     }
474
475     protected Set<PersistenceItemInfo> getItems() {
476         // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
477         // would be a very costly operation
478         return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
479                 .collect(Collectors.<PersistenceItemInfo> toSet());
480     }
481
482     /*****************
483      * H E L P E R S *
484      *****************/
485     private void logTime(String me, long timerStart, long timerStop) {
486         if (conf.enableLogTime && logger.isInfoEnabled()) {
487             conf.timerCount++;
488             int timerDiff = (int) (timerStop - timerStart);
489             if (timerDiff < afterAccessMin) {
490                 afterAccessMin = timerDiff;
491             }
492             if (timerDiff > afterAccessMax) {
493                 afterAccessMax = timerDiff;
494             }
495             conf.timeAverage50arr.add(timerDiff);
496             conf.timeAverage100arr.add(timerDiff);
497             conf.timeAverage200arr.add(timerDiff);
498             if (conf.timerCount == 1) {
499                 conf.timer1000 = System.currentTimeMillis();
500             }
501             if (conf.timerCount == 1001) {
502                 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
503                 conf.timerCount = 0;
504             }
505             logger.info(
506                     "JDBC::logTime: '{}':\n afterAccess     = {} ms\n timeAverage50  = {} ms\n timeAverage100 = {} ms\n timeAverage200 = {} ms\n afterAccessMin  = {} ms\n afterAccessMax  = {} ms\n 1000Statements = {} sec\n statementCount = {}\n",
507                     me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
508                     conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
509                     afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);
510         }
511     }
512 }