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