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