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