2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.jdbc.internal;
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;
22 import java.util.stream.Collectors;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.knowm.yank.Yank;
27 import org.openhab.core.i18n.TimeZoneProvider;
28 import org.openhab.core.items.Item;
29 import org.openhab.core.persistence.FilterCriteria;
30 import org.openhab.core.persistence.HistoricItem;
31 import org.openhab.core.persistence.PersistenceItemInfo;
32 import org.openhab.core.types.State;
33 import org.openhab.persistence.jdbc.dto.ItemVO;
34 import org.openhab.persistence.jdbc.dto.ItemsVO;
35 import org.openhab.persistence.jdbc.dto.JdbcPersistenceItemInfo;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
44 * @author Helmut Lehmeyer - Initial contribution
47 public class JdbcMapper {
48 private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
50 private final TimeZoneProvider timeZoneProvider;
52 // Error counter - used to reconnect to database on error
54 protected boolean initialized = false;
55 protected @NonNullByDefault({}) JdbcConfiguration conf;
56 protected final Map<String, String> sqlTables = new HashMap<>();
57 private long afterAccessMin = 10000;
58 private long afterAccessMax = 0;
59 private static final String ITEM_NAME_PATTERN = "[^a-zA-Z_0-9\\-]";
61 public JdbcMapper(TimeZoneProvider timeZoneProvider) {
62 this.timeZoneProvider = timeZoneProvider;
68 public boolean pingDB() {
69 logger.debug("JDBC::pingDB");
71 long timerStart = System.currentTimeMillis();
72 if (openConnection()) {
73 if (conf.getDbName() == null) {
75 "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
76 String dbName = conf.getDBDAO().doGetDB();
80 conf.setDbName(dbName);
81 ret = dbName.length() > 0;
84 final @Nullable Integer result = conf.getDBDAO().doPingDB();
85 ret = result != null && result > 0;
88 logTime("pingDB", timerStart, System.currentTimeMillis());
92 public String getDB() {
93 logger.debug("JDBC::getDB");
94 long timerStart = System.currentTimeMillis();
95 String res = conf.getDBDAO().doGetDB();
96 logTime("getDB", timerStart, System.currentTimeMillis());
97 return res != null ? res : "";
100 public ItemsVO createNewEntryInItemsTable(ItemsVO vo) {
101 logger.debug("JDBC::createNewEntryInItemsTable");
102 long timerStart = System.currentTimeMillis();
103 Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
104 vo.setItemid(i.intValue());
105 logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
109 public boolean createItemsTableIfNot(ItemsVO vo) {
110 logger.debug("JDBC::createItemsTableIfNot");
111 long timerStart = System.currentTimeMillis();
112 conf.getDBDAO().doCreateItemsTableIfNot(vo);
113 logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
117 public ItemsVO deleteItemsEntry(ItemsVO vo) {
118 logger.debug("JDBC::deleteItemsEntry");
119 long timerStart = System.currentTimeMillis();
120 conf.getDBDAO().doDeleteItemsEntry(vo);
121 logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
125 public List<ItemsVO> getItemIDTableNames() {
126 logger.debug("JDBC::getItemIDTableNames");
127 long timerStart = System.currentTimeMillis();
128 List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(new ItemsVO());
129 logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
133 public List<ItemsVO> getItemTables() {
134 logger.debug("JDBC::getItemTables");
135 long timerStart = System.currentTimeMillis();
136 ItemsVO vo = new ItemsVO();
137 vo.setJdbcUriDatabaseName(conf.getDbName());
138 List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(vo);
139 logTime("getItemTables", timerStart, System.currentTimeMillis());
146 public void updateItemTableNames(List<ItemVO> vol) {
147 logger.debug("JDBC::updateItemTableNames");
148 long timerStart = System.currentTimeMillis();
149 conf.getDBDAO().doUpdateItemTableNames(vol);
150 logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
153 public ItemVO createItemTable(ItemVO vo) {
154 logger.debug("JDBC::createItemTable");
155 long timerStart = System.currentTimeMillis();
156 conf.getDBDAO().doCreateItemTable(vo);
157 logTime("createItemTable", timerStart, System.currentTimeMillis());
161 public Item storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) {
162 logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
163 String tableName = getTable(item);
164 long timerStart = System.currentTimeMillis();
166 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
168 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
170 logTime("storeItemValue", timerStart, System.currentTimeMillis());
175 public List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
178 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
179 true, numberDecimalcount, table, item, item.getName());
180 long timerStart = System.currentTimeMillis();
181 List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
182 item.getName(), timeZoneProvider.getTimeZone());
183 logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
188 public boolean deleteItemValues(FilterCriteria filter, String table) {
189 logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
190 long timerStart = System.currentTimeMillis();
191 conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
192 logTime("deleteItemValues", timerStart, System.currentTimeMillis());
197 /***********************
198 * DATABASE CONNECTION *
199 ***********************/
200 protected boolean openConnection() {
201 logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
202 if (conf.isDriverAvailable() && !conf.isDbConnected()) {
203 logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
205 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
206 conf.setDbConnected(true);
208 } catch (PoolInitializationException e) {
209 Throwable cause = e.getCause();
210 if (cause instanceof SQLInvalidAuthorizationSpecException) {
211 logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
213 logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
218 } else if (!conf.isDriverAvailable()) {
219 logger.warn("JDBC::openConnection: no driver available!");
226 protected void closeConnection() {
227 logger.debug("JDBC::closeConnection");
228 // Closes all open connection pools
229 Yank.releaseDefaultConnectionPool();
230 conf.setDbConnected(false);
233 protected boolean checkDBAccessability() {
234 // Check if connection is valid
239 boolean p = pingDB();
241 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
242 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
246 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
247 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
251 /**************************
252 * DATABASE TABLEHANDLING *
253 **************************/
254 protected void checkDBSchema() {
255 // Create Items Table if does not exist
256 createItemsTableIfNot(new ItemsVO());
257 if (conf.getRebuildTableNames()) {
260 "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
262 // Reset the error counter
264 for (ItemsVO vo : getItemIDTableNames()) {
265 sqlTables.put(vo.getItemname(), getTableName(vo.getItemid(), vo.getItemname()));
270 protected String getTable(Item item) {
275 String itemName = item.getName();
276 String tableName = sqlTables.get(itemName);
278 // Table already exists - return the name
279 if (tableName != null) {
283 logger.debug("JDBC::getTable: no table found for item '{}' in sqlTables", itemName);
285 // Create a new entry in items table
286 isvo = new ItemsVO();
287 isvo.setItemname(itemName);
288 isvo = createNewEntryInItemsTable(isvo);
289 rowId = isvo.getItemid();
291 logger.error("JDBC::getTable: Creating table for item '{}' failed.", itemName);
293 // Create the table name
294 logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", rowId, itemName);
295 tableName = getTableName(rowId, itemName);
297 // Create table for item
298 String dataType = conf.getDBDAO().getDataType(item);
299 ivo = new ItemVO(tableName, itemName);
300 ivo.setDbType(dataType);
301 ivo = createItemTable(ivo);
302 logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
304 sqlTables.put(itemName, tableName);
306 // Check if the new entry is in the table list
307 // If it's not in the list, then there was an error and we need to do
309 // The item needs to be removed from the index table to avoid duplicates
310 if (sqlTables.get(itemName) == null) {
311 logger.error("JDBC::getTable: Item '{}' was not added to the table - removing index", itemName);
312 isvo = new ItemsVO();
313 isvo.setItemname(itemName);
314 deleteItemsEntry(isvo);
320 private void formatTableNames() {
321 boolean tmpinit = initialized;
326 Map<Integer, String> tableIds = new HashMap<>();
329 for (ItemsVO vo : getItemIDTableNames()) {
330 String t = getTableName(vo.getItemid(), vo.getItemname());
331 sqlTables.put(vo.getItemname(), t);
332 tableIds.put(vo.getItemid(), t);
336 List<ItemsVO> al = getItemTables();
340 List<ItemVO> oldNewTablenames = new ArrayList<>();
341 for (int i = 0; i < al.size(); i++) {
343 oldName = al.get(i).getTable_name();
344 logger.info("JDBC::formatTableNames: found Table Name= {}", oldName);
346 if (oldName.startsWith(conf.getTableNamePrefix()) && !oldName.contains("_")) {
347 id = Integer.parseInt(oldName.substring(conf.getTableNamePrefix().length()));
348 logger.info("JDBC::formatTableNames: found Table with Prefix '{}' Name= {} id= {}",
349 conf.getTableNamePrefix(), oldName, (id));
350 } else if (oldName.contains("_")) {
351 id = Integer.parseInt(oldName.substring(oldName.lastIndexOf("_") + 1));
352 logger.info("JDBC::formatTableNames: found Table Name= {} id= {}", oldName, (id));
354 logger.info("JDBC::formatTableNames: found Table id= {}", id);
356 newName = tableIds.get(id);
357 logger.info("JDBC::formatTableNames: found Table newName= {}", newName);
359 if (newName != null) {
360 if (!oldName.equalsIgnoreCase(newName)) {
361 oldNewTablenames.add(new ItemVO(oldName, newName));
362 logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", oldName, newName);
364 logger.info("JDBC::formatTableNames: Table oldName='{}' newName='{}' nothing to rename", oldName,
368 logger.error("JDBC::formatTableNames: Table '{}' could NOT be renamed to '{}'", oldName, newName);
373 updateItemTableNames(oldNewTablenames);
374 logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTablenames.size());
376 initialized = tmpinit;
379 private String getTableName(int rowId, String itemName) {
380 return getTableNamePrefix(itemName) + formatRight(rowId, conf.getTableIdDigitCount());
383 private String getTableNamePrefix(String itemName) {
384 String name = conf.getTableNamePrefix();
385 if (conf.getTableUseRealItemNames()) {
386 // Create the table name with real Item Names
387 name = (itemName.replaceAll(ITEM_NAME_PATTERN, "") + "_").toLowerCase();
392 public Set<PersistenceItemInfo> getItems() {
393 // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
394 // would be a very costly operation
395 return sqlTables.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
396 .collect(Collectors.<PersistenceItemInfo> toSet());
399 private static String formatRight(final Object value, final int len) {
400 final String valueAsString = String.valueOf(value);
401 if (valueAsString.length() < len) {
402 final StringBuffer result = new StringBuffer(len);
403 for (int i = len - valueAsString.length(); i > 0; i--) {
406 result.append(valueAsString);
407 return result.toString();
409 return valueAsString;
416 private void logTime(String me, long timerStart, long timerStop) {
417 if (conf.enableLogTime && logger.isInfoEnabled()) {
419 int timerDiff = (int) (timerStop - timerStart);
420 if (timerDiff < afterAccessMin) {
421 afterAccessMin = timerDiff;
423 if (timerDiff > afterAccessMax) {
424 afterAccessMax = timerDiff;
426 conf.timeAverage50arr.add(timerDiff);
427 conf.timeAverage100arr.add(timerDiff);
428 conf.timeAverage200arr.add(timerDiff);
429 if (conf.timerCount == 1) {
430 conf.timer1000 = System.currentTimeMillis();
432 if (conf.timerCount == 1001) {
433 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
437 "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",
438 me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
439 conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
440 afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);