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