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