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