From bb2e4c7c653bdba0b5cc865a509d18ea4c903be1 Mon Sep 17 00:00:00 2001 From: Christoph Weitkamp Date: Sat, 15 Jan 2022 14:29:06 +0100 Subject: [PATCH] [jdbc] Implement 'ModifiablePersistenceService' interface (#11922) Signed-off-by: Christoph Weitkamp --- .../persistence/jdbc/db/JdbcBaseDAO.java | 98 ++++++++----- .../persistence/jdbc/db/JdbcDerbyDAO.java | 12 +- .../persistence/jdbc/db/JdbcH2DAO.java | 11 +- .../persistence/jdbc/db/JdbcHsqldbDAO.java | 14 +- .../jdbc/db/JdbcPostgresqlDAO.java | 11 +- .../persistence/jdbc/db/JdbcSqliteDAO.java | 11 +- .../openhab/persistence/jdbc/dto/ItemVO.java | 30 +--- .../persistence/jdbc/internal/JdbcMapper.java | 28 +++- .../jdbc/internal/JdbcPersistenceService.java | 79 ++++++++-- .../persistence/jdbc/db/JdbcBaseDAOTest.java | 135 ++++++++++++++++++ .../internal/JdbcPersistenceServiceTest.java | 51 +++++++ 11 files changed, 382 insertions(+), 98 deletions(-) create mode 100644 bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/db/JdbcBaseDAOTest.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceTest.java diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index b53988293..9f8087066 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -13,7 +13,6 @@ package org.openhab.persistence.jdbc.db; import java.math.BigDecimal; -import java.sql.Timestamp; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -327,8 +326,8 @@ public class JdbcBaseDAO { Yank.execute(sql, null); } - public void doStoreItemValue(Item item, ItemVO vo) { - ItemVO storedVO = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, new String[] { "#tableName#", "#tablePrimaryValue#" }, new String[] { storedVO.getTableName(), sqlTypes.get("tablePrimaryValue") }); @@ -337,6 +336,16 @@ public class JdbcBaseDAO { Yank.execute(sql, params); } + public void doStoreItemValue(Item item, State itemState, ItemVO vo, ZonedDateTime date) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); + String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, + new String[] { "#tableName#", "#tablePrimaryValue#" }, new String[] { storedVO.getTableName(), "?" }); + java.sql.Timestamp timestamp = new java.sql.Timestamp(date.toInstant().toEpochMilli()); + Object[] params = new Object[] { storedVO.getValue(), timestamp, storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} timestamp={} value='{}'", sql, timestamp, storedVO.getValue()); + Yank.execute(sql, params); + } + public List doGetHistItemFilterQuery(Item item, FilterCriteria filter, int numberDecimalcount, String table, String name, ZoneId timeZone) { String sql = histItemFilterQueryProvider(filter, numberDecimalcount, table, name, timeZone); @@ -353,6 +362,12 @@ public class JdbcBaseDAO { .collect(Collectors. toList()); } + public void doDeleteItemValues(Item item, FilterCriteria filter, String table, ZoneId timeZone) { + String sql = histItemFilterDeleteProvider(filter, table, timeZone); + logger.debug("JDBC::doDeleteItemValues sql={}", sql); + Yank.execute(sql, null); + } + /************* * Providers * *************/ @@ -364,19 +379,9 @@ public class JdbcBaseDAO { "JDBC::getHistItemFilterQueryProvider filter = {}, numberDecimalcount = {}, table = {}, simpleName = {}", filter, numberDecimalcount, table, simpleName); - String filterString = ""; - if (filter.getBeginDate() != null) { - filterString += filterString.isEmpty() ? " WHERE" : " AND"; - filterString += " TIME>'" + JDBC_DATE_FORMAT.format(filter.getBeginDate().withZoneSameInstant(timeZone)) - + "'"; - } - if (filter.getEndDate() != null) { - filterString += filterString.isEmpty() ? " WHERE" : " AND"; - filterString += " TIME<'" + JDBC_DATE_FORMAT.format(filter.getEndDate().withZoneSameInstant(timeZone)) - + "'"; - } - filterString += (filter.getOrdering() == Ordering.ASCENDING) ? " ORDER BY time ASC" : " ORDER BY time DESC "; - if (filter.getPageSize() != 0x7fffffff) { + String filterString = resolveTimeFilter(filter, timeZone); + filterString += (filter.getOrdering() == Ordering.ASCENDING) ? " ORDER BY time ASC" : " ORDER BY time DESC"; + if (filter.getPageSize() != Integer.MAX_VALUE) { filterString += " LIMIT " + filter.getPageNumber() * filter.getPageSize() + "," + filter.getPageSize(); } // SELECT time, ROUND(value,3) FROM number_item_0114 ORDER BY time DESC LIMIT 0,1 @@ -391,6 +396,33 @@ public class JdbcBaseDAO { return queryString; } + protected String histItemFilterDeleteProvider(FilterCriteria filter, String table, ZoneId timeZone) { + logger.debug("JDBC::histItemFilterDeleteProvider filter = {}, table = {}", filter, table); + + String filterString = resolveTimeFilter(filter, timeZone); + String deleteString = "DELETE FROM " + table; + if (!filterString.isEmpty()) { + deleteString += filterString; + } + logger.debug("JDBC::delete deleteString = {}", deleteString); + return deleteString; + } + + protected String resolveTimeFilter(FilterCriteria filter, ZoneId timeZone) { + String filterString = ""; + if (filter.getBeginDate() != null) { + filterString += filterString.isEmpty() ? " WHERE" : " AND"; + filterString += " TIME>'" + JDBC_DATE_FORMAT.format(filter.getBeginDate().withZoneSameInstant(timeZone)) + + "'"; + } + if (filter.getEndDate() != null) { + filterString += filterString.isEmpty() ? " WHERE" : " AND"; + filterString += " TIME<'" + JDBC_DATE_FORMAT.format(filter.getEndDate().withZoneSameInstant(timeZone)) + + "'"; + } + return filterString; + } + private String updateItemTableNamesProvider(List namesList) { logger.debug("JDBC::updateItemTableNamesProvider namesList.size = {}", namesList.size()); String queryString = ""; @@ -402,14 +434,14 @@ public class JdbcBaseDAO { return queryString; } - protected ItemVO storeItemValueProvider(Item item, ItemVO vo) { + protected ItemVO storeItemValueProvider(Item item, State itemState, ItemVO vo) { String itemType = getItemType(item); logger.debug("JDBC::storeItemValueProvider: item '{}' as Type '{}' in '{}' with state '{}'", item.getName(), - itemType, vo.getTableName(), item.getState()); + itemType, vo.getTableName(), itemState); // insertItemValue - logger.debug("JDBC::storeItemValueProvider: getState: '{}'", item.getState()); + logger.debug("JDBC::storeItemValueProvider: itemState: '{}'", itemState); /* * !!ATTENTION!! * @@ -427,20 +459,19 @@ public class JdbcBaseDAO { switch (itemType) { case "COLORITEM": vo.setValueTypes(getSqlTypes().get(itemType), java.lang.String.class); - vo.setValue(item.getState().toString()); + vo.setValue(itemState.toString()); break; case "NUMBERITEM": - State state = item.getState(); - State convertedState = state; - if (item instanceof NumberItem && state instanceof QuantityType) { + State convertedState = itemState; + if (item instanceof NumberItem && itemState instanceof QuantityType) { Unit> unit = ((NumberItem) item).getUnit(); if (unit != null && !Units.ONE.equals(unit)) { - convertedState = ((QuantityType) state).toUnit(unit); + convertedState = ((QuantityType) itemState).toUnit(unit); if (convertedState == null) { logger.warn( "JDBC::storeItemValueProvider: Failed to convert state '{}' to unit '{}'. Please check your item definition for correctness.", - state, unit); - convertedState = state; + itemState, unit); + convertedState = itemState; } } } @@ -462,21 +493,21 @@ public class JdbcBaseDAO { vo.setValue(value); } else {// fall back to String vo.setValueTypes(it, java.lang.String.class); - logger.warn("JDBC::storeItemValueProvider: item.getState().toString(): '{}'", convertedState); + logger.warn("JDBC::storeItemValueProvider: itemState: '{}'", convertedState); vo.setValue(convertedState.toString()); } break; case "ROLLERSHUTTERITEM": case "DIMMERITEM": vo.setValueTypes(getSqlTypes().get(itemType), java.lang.Integer.class); - int value = ((DecimalType) item.getState()).intValue(); + int value = ((DecimalType) itemState).intValue(); logger.debug("JDBC::storeItemValueProvider: newVal.intValue: '{}'", value); vo.setValue(value); break; case "DATETIMEITEM": vo.setValueTypes(getSqlTypes().get(itemType), java.sql.Timestamp.class); java.sql.Timestamp d = new java.sql.Timestamp( - ((DateTimeType) item.getState()).getZonedDateTime().toInstant().toEpochMilli()); + ((DateTimeType) itemState).getZonedDateTime().toInstant().toEpochMilli()); logger.debug("JDBC::storeItemValueProvider: DateTimeItem: '{}'", d); vo.setValue(d); break; @@ -489,8 +520,8 @@ public class JdbcBaseDAO { default: // All other items should return the best format by default vo.setValueTypes(getSqlTypes().get(itemType), java.lang.String.class); - logger.debug("JDBC::storeItemValueProvider: other: item.getState().toString(): '{}'", item.getState()); - vo.setValue(item.getState().toString()); + logger.debug("JDBC::storeItemValueProvider: other: itemState: '{}'", itemState); + vo.setValue(itemState.toString()); break; } return vo; @@ -533,9 +564,10 @@ public class JdbcBaseDAO { protected ZonedDateTime objectAsDate(Object v) { if (v instanceof java.lang.String) { - return ZonedDateTime.ofInstant(Timestamp.valueOf(v.toString()).toInstant(), ZoneId.systemDefault()); + return ZonedDateTime.ofInstant(java.sql.Timestamp.valueOf(v.toString()).toInstant(), + ZoneId.systemDefault()); } - return ZonedDateTime.ofInstant(((Timestamp) v).toInstant(), ZoneId.systemDefault()); + return ZonedDateTime.ofInstant(((java.sql.Timestamp) v).toInstant(), ZoneId.systemDefault()); } protected Long objectAsLong(Object v) { diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java index 148ee2414..6e18c38b0 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java @@ -25,6 +25,7 @@ import org.openhab.core.library.items.NumberItem; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.FilterCriteria.Ordering; import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.dto.ItemsVO; import org.openhab.persistence.jdbc.dto.JdbcHistoricItem; @@ -148,13 +149,14 @@ public class JdbcDerbyDAO extends JdbcBaseDAO { } @Override - public void doStoreItemValue(Item item, ItemVO vo) { - vo = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" }, - new String[] { vo.getTableName().toUpperCase(), vo.getDbType(), sqlTypes.get("tablePrimaryValue") }); - Object[] params = new Object[] { vo.getValue() }; - logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, vo.getValue()); + new String[] { storedVO.getTableName().toUpperCase(), storedVO.getDbType(), + sqlTypes.get("tablePrimaryValue") }); + Object[] params = new Object[] { storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue()); Yank.execute(sql, params); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcH2DAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcH2DAO.java index 8957f1b45..6fa73951f 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcH2DAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcH2DAO.java @@ -14,6 +14,7 @@ package org.openhab.persistence.jdbc.db; import org.knowm.yank.Yank; import org.openhab.core.items.Item; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.utils.StringUtilsExt; import org.slf4j.Logger; @@ -71,13 +72,13 @@ public class JdbcH2DAO extends JdbcBaseDAO { * ITEM DAOs * *************/ @Override - public void doStoreItemValue(Item item, ItemVO vo) { - vo = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" }, - new String[] { vo.getTableName(), vo.getDbType(), sqlTypes.get("tablePrimaryValue") }); - Object[] params = new Object[] { vo.getValue() }; - logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, vo.getValue()); + new String[] { storedVO.getTableName(), storedVO.getDbType(), sqlTypes.get("tablePrimaryValue") }); + Object[] params = new Object[] { storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue()); Yank.execute(sql, params); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java index 5e92d3c7e..dbc9486f6 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java @@ -14,6 +14,7 @@ package org.openhab.persistence.jdbc.db; import org.knowm.yank.Yank; import org.openhab.core.items.Item; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.dto.ItemsVO; import org.openhab.persistence.jdbc.utils.StringUtilsExt; @@ -101,13 +102,14 @@ public class JdbcHsqldbDAO extends JdbcBaseDAO { * ITEM DAOs * *************/ @Override - public void doStoreItemValue(Item item, ItemVO vo) { - vo = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, - new String[] { "#tableName#", "#dbType#", "#tableName#", "#tablePrimaryValue#" }, new String[] { - vo.getTableName(), vo.getDbType(), vo.getTableName(), sqlTypes.get("tablePrimaryValue") }); - Object[] params = new Object[] { vo.getValue() }; - logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, vo.getValue()); + new String[] { "#tableName#", "#dbType#", "#tableName#", "#tablePrimaryValue#" }, + new String[] { storedVO.getTableName(), storedVO.getDbType(), storedVO.getTableName(), + sqlTypes.get("tablePrimaryValue") }); + Object[] params = new Object[] { storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue()); Yank.execute(sql, params); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java index d83295783..53a1c4ba6 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java @@ -19,6 +19,7 @@ import org.knowm.yank.Yank; import org.openhab.core.items.Item; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.FilterCriteria.Ordering; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.dto.ItemsVO; import org.openhab.persistence.jdbc.utils.StringUtilsExt; @@ -133,13 +134,13 @@ public class JdbcPostgresqlDAO extends JdbcBaseDAO { * ITEM DAOs * *************/ @Override - public void doStoreItemValue(Item item, ItemVO vo) { - vo = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" }, - new String[] { vo.getTableName(), vo.getDbType(), sqlTypes.get("tablePrimaryValue") }); - Object[] params = new Object[] { vo.getValue() }; - logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, vo.getValue()); + new String[] { storedVO.getTableName(), storedVO.getDbType(), sqlTypes.get("tablePrimaryValue") }); + Object[] params = new Object[] { storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue()); Yank.execute(sql, params); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcSqliteDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcSqliteDAO.java index 5cf5c0ec2..e5665e2c0 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcSqliteDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcSqliteDAO.java @@ -14,6 +14,7 @@ package org.openhab.persistence.jdbc.db; import org.knowm.yank.Yank; import org.openhab.core.items.Item; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.dto.ItemsVO; import org.openhab.persistence.jdbc.utils.StringUtilsExt; @@ -90,13 +91,13 @@ public class JdbcSqliteDAO extends JdbcBaseDAO { * ITEM DAOs * *************/ @Override - public void doStoreItemValue(Item item, ItemVO vo) { - vo = storeItemValueProvider(item, vo); + public void doStoreItemValue(Item item, State itemState, ItemVO vo) { + ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" }, - new String[] { vo.getTableName(), vo.getDbType(), sqlTypes.get("tablePrimaryValue") }); - Object[] params = new Object[] { vo.getValue() }; - logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, vo.getValue()); + new String[] { storedVO.getTableName(), storedVO.getDbType(), sqlTypes.get("tablePrimaryValue") }); + Object[] params = new Object[] { storedVO.getValue() }; + logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue()); Yank.execute(sql, params); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemVO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemVO.java index 9c7f91ca9..af4a03ba3 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemVO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemVO.java @@ -16,6 +16,7 @@ import java.io.Serializable; import java.util.Date; import java.util.Objects; +import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,7 +31,7 @@ public class ItemVO implements Serializable { private static final long serialVersionUID = 1871441039821454890L; private String tableName; - private String newTableName; + private @Nullable String newTableName; private String dbType; private String jdbcType; private String itemType; @@ -38,7 +39,7 @@ public class ItemVO implements Serializable { private Date time; private Object value; - public ItemVO(String tableName, String newTableName) { + public ItemVO(String tableName, @Nullable String newTableName) { logger.debug("JDBC:ItemVO tableName={}; newTableName={}; ", tableName, newTableName); this.tableName = tableName; this.newTableName = newTableName; @@ -61,7 +62,7 @@ public class ItemVO implements Serializable { this.tableName = tableName; } - public String getNewTableName() { + public @Nullable String getNewTableName() { return newTableName; } @@ -117,11 +118,6 @@ public class ItemVO implements Serializable { this.value = value; } - /** - * (non-Javadoc) - * - * @see java.lang.Object#equals(java.lang.Object) - */ @Override public boolean equals(Object obj) { if (this == obj) { @@ -146,20 +142,8 @@ public class ItemVO implements Serializable { @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("ItemVO [tableName="); - builder.append(tableName); - builder.append(", newTableName="); - builder.append(newTableName); - builder.append(", dbType="); - builder.append(dbType); - builder.append(", javaType="); - builder.append(javaType); - builder.append(", time="); - builder.append(time); - builder.append(", value="); - builder.append(value); - builder.append("]"); - return builder.toString(); + return new StringBuilder("ItemVO [tableName=").append(tableName).append(", newTableName=").append(newTableName) + .append(", dbType=").append(dbType).append(", javaType=").append(javaType).append(", time=") + .append(time).append(", value=").append(value).append("]").toString(); } } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java index 1bf53f1fb..9476ba542 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java @@ -12,6 +12,7 @@ */ package org.openhab.persistence.jdbc.internal; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -19,12 +20,14 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.jdt.annotation.Nullable; import org.knowm.yank.Yank; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.persistence.PersistenceItemInfo; +import org.openhab.core.types.State; import org.openhab.persistence.jdbc.dto.ItemVO; import org.openhab.persistence.jdbc.dto.ItemsVO; import org.openhab.persistence.jdbc.dto.JdbcPersistenceItemInfo; @@ -145,15 +148,19 @@ public class JdbcMapper { return vo; } - public Item storeItemValue(Item item) { - logger.debug("JDBC::storeItemValue: item={}", item); + public Item storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) { + logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date); String tableName = getTable(item); if (tableName == null) { logger.error("JDBC::store: Unable to store item '{}'.", item.getName()); return item; } long timerStart = System.currentTimeMillis(); - conf.getDBDAO().doStoreItemValue(item, new ItemVO(tableName, null)); + if (date == null) { + conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null)); + } else { + conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date); + } logTime("storeItemValue", timerStart, System.currentTimeMillis()); errCnt = 0; return item; @@ -177,6 +184,21 @@ public class JdbcMapper { return null; } + public boolean deleteItemValues(FilterCriteria filter, String table, Item item) { + logger.debug("JDBC::deleteItemValues filter='{}' table='{}' item='{}' itemName='{}'", (filter != null), table, + item, item.getName()); + if (table != null) { + long timerStart = System.currentTimeMillis(); + conf.getDBDAO().doDeleteItemValues(item, filter, table, timeZoneProvider.getTimeZone()); + logTime("deleteItemValues", timerStart, System.currentTimeMillis()); + errCnt = 0; + return true; + } else { + logger.error("JDBC::deleteItemValues: TABLE is NULL; cannot delete data from non-existent table."); + return false; + } + } + /*********************** * DATABASE CONNECTION * ***********************/ diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java index 4feb40b6f..c00e178a8 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java @@ -12,6 +12,8 @@ */ package org.openhab.persistence.jdbc.internal; +import java.time.ZonedDateTime; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @@ -27,10 +29,12 @@ import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.ModifiablePersistenceService; import org.openhab.core.persistence.PersistenceItemInfo; import org.openhab.core.persistence.PersistenceService; import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; @@ -52,8 +56,10 @@ import org.slf4j.LoggerFactory; QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", // property = Constants.SERVICE_PID + "=org.openhab.jdbc") @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceService.CONFIG_URI) -public class JdbcPersistenceService extends JdbcMapper implements QueryablePersistenceService { +public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService { + private static final String SERVICE_ID = "jdbc"; + private static final String SERVICE_LABEL = "JDBC"; protected static final String CONFIG_URI = "persistence:jdbc"; private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class); @@ -110,39 +116,48 @@ public class JdbcPersistenceService extends JdbcMapper implements QueryablePersi @Override public String getId() { logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service."); - return "jdbc"; + return SERVICE_ID; } @Override public String getLabel(@Nullable Locale locale) { - return "JDBC"; + return SERVICE_LABEL; } @Override public void store(Item item) { - store(item, null); + internalStore(item, null, item.getState()); } - /** - * @{inheritDoc - */ @Override public void store(Item item, @Nullable String alias) { + // alias is not supported + internalStore(item, null, item.getState()); + } + + @Override + public void store(Item item, ZonedDateTime date, State state) { + internalStore(item, date, state); + } + + private void internalStore(Item item, @Nullable ZonedDateTime date, State state) { // Do not store undefined/uninitialized data - if (item.getState() instanceof UnDefType) { + if (state instanceof UnDefType) { logger.debug("JDBC::store: ignore Item '{}' because it is UnDefType", item.getName()); return; } if (!checkDBAccessability()) { logger.warn( - "JDBC::store: No connection to database. Cannot persist item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}", - item, errCnt, conf.getErrReconnectThreshold()); + "JDBC::store: No connection to database. Cannot persist state '{}' for item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}", + state, item, errCnt, conf.getErrReconnectThreshold()); return; } long timerStart = System.currentTimeMillis(); - storeItemValue(item); - logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), item.getState(), - new java.util.Date(), System.currentTimeMillis() - timerStart); + storeItemValue(item, state, date); + if (logger.isDebugEnabled()) { + logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), state, + new Date(), System.currentTimeMillis() - timerStart); + } } @Override @@ -228,4 +243,42 @@ public class JdbcPersistenceService extends JdbcMapper implements QueryablePersi public List getDefaultStrategies() { return List.of(PersistenceStrategy.Globals.CHANGE); } + + @Override + public boolean remove(FilterCriteria filter) throws IllegalArgumentException { + if (!checkDBAccessability()) { + logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName()); + return false; + } + + // Get the item name from the filter + // Also get the Item object so we can determine the type + Item item = null; + String itemName = filter.getItemName(); + logger.debug("JDBC::remove: item is {}", itemName); + if (itemName == null) { + throw new IllegalArgumentException("Item name must not be null"); + } + try { + item = itemRegistry.getItem(itemName); + } catch (ItemNotFoundException e) { + logger.error("JDBC::remove: unable to get item for itemName: '{}'. Ignore and give up!", itemName); + return false; + } + + String table = sqlTables.get(itemName); + if (table == null) { + logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName); + return false; + } + + long timerStart = System.currentTimeMillis(); + boolean result = deleteItemValues(filter, table, item); + if (logger.isDebugEnabled()) { + logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", item.getName(), + new Date(), System.currentTimeMillis() - timerStart); + } + + return result; + } } diff --git a/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/db/JdbcBaseDAOTest.java b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/db/JdbcBaseDAOTest.java new file mode 100644 index 000000000..1b58e6243 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/db/JdbcBaseDAOTest.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.db; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.persistence.FilterCriteria; +import org.openhab.core.persistence.FilterCriteria.Ordering; + +/** + * Tests the {@link JdbcBaseDAO}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class JdbcBaseDAOTest { + + private static final String DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + private static final DateTimeFormatter DATE_PARSER = DateTimeFormatter.ofPattern(DATE_PATTERN); + private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); + private static final String DB_TABLE_NAME = "testitem"; + + private final JdbcBaseDAO jdbcBaseDAO = new JdbcBaseDAO(); + private @NonNullByDefault({}) FilterCriteria filter; + + @BeforeEach + public void setup() { + filter = new FilterCriteria(); + } + + @Test + public void testHistItemFilterQueryProviderReturnsSelectQueryWithoutWhereClauseDescendingOrder() { + String sql = jdbcBaseDAO.histItemFilterQueryProvider(filter, 0, DB_TABLE_NAME, "TEST", UTC_ZONE_ID); + assertThat(sql, is("SELECT time, value FROM " + DB_TABLE_NAME + " ORDER BY time DESC")); + } + + @Test + public void testHistItemFilterQueryProviderReturnsSelectQueryWithoutWhereClauseAscendingOrder() { + filter.setOrdering(Ordering.ASCENDING); + + String sql = jdbcBaseDAO.histItemFilterQueryProvider(filter, 0, DB_TABLE_NAME, "TEST", UTC_ZONE_ID); + assertThat(sql, is("SELECT time, value FROM " + DB_TABLE_NAME + " ORDER BY time ASC")); + } + + @Test + public void testHistItemFilterQueryProviderWithStartAndEndDateReturnsDeleteQueryWithWhereClauseDescendingOrder() { + filter.setBeginDate(parseDateTimeString("2022-01-10T15:01:44")); + filter.setEndDate(parseDateTimeString("2022-01-15T15:01:44")); + + String sql = jdbcBaseDAO.histItemFilterQueryProvider(filter, 0, DB_TABLE_NAME, "TEST", UTC_ZONE_ID); + assertThat(sql, is("SELECT time, value FROM " + DB_TABLE_NAME + " WHERE TIME>'" // + + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getBeginDate()) + "'" // + + " AND TIME<'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getEndDate()) + "' ORDER BY time DESC")); + } + + @Test + public void testHistItemFilterQueryProviderReturnsSelectQueryWithoutWhereClauseDescendingOrderAndLimit() { + filter.setPageSize(1); + + String sql = jdbcBaseDAO.histItemFilterQueryProvider(filter, 0, DB_TABLE_NAME, "TEST", UTC_ZONE_ID); + assertThat(sql, is("SELECT time, value FROM " + DB_TABLE_NAME + " ORDER BY time DESC LIMIT 0,1")); + } + + @Test + public void testHistItemFilterDeleteProviderReturnsDeleteQueryWithoutWhereClause() { + String sql = jdbcBaseDAO.histItemFilterDeleteProvider(filter, DB_TABLE_NAME, UTC_ZONE_ID); + assertThat(sql, is("DELETE FROM " + DB_TABLE_NAME)); + } + + @Test + public void testHistItemFilterDeleteProviderWithStartAndEndDateReturnsDeleteQueryWithWhereClause() { + filter.setBeginDate(parseDateTimeString("2022-01-10T15:01:44")); + filter.setEndDate(parseDateTimeString("2022-01-15T15:01:44")); + + String sql = jdbcBaseDAO.histItemFilterDeleteProvider(filter, DB_TABLE_NAME, UTC_ZONE_ID); + assertThat(sql, is("DELETE FROM " + DB_TABLE_NAME + " WHERE TIME>'" // + + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getBeginDate()) + "'" // + + " AND TIME<'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getEndDate()) + "'")); + } + + @Test + public void testResolveTimeFilterWithNoDatesReturnsEmptyString() { + String sql = jdbcBaseDAO.resolveTimeFilter(filter, UTC_ZONE_ID); + assertThat(sql, is("")); + } + + @Test + public void testResolveTimeFilterWithStartDateOnlyReturnsWhereClause() { + filter.setBeginDate(parseDateTimeString("2022-01-10T15:01:44")); + + String sql = jdbcBaseDAO.resolveTimeFilter(filter, UTC_ZONE_ID); + assertThat(sql, is(" WHERE TIME>'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getBeginDate()) + "'")); + } + + @Test + public void testResolveTimeFilterWithEndDateOnlyReturnsWhereClause() { + filter.setEndDate(parseDateTimeString("2022-01-15T15:01:44")); + + String sql = jdbcBaseDAO.resolveTimeFilter(filter, UTC_ZONE_ID); + assertThat(sql, is(" WHERE TIME<'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getEndDate()) + "'")); + } + + @Test + public void testResolveTimeFilterWithStartAndEndDateReturnsWhereClauseWithTwoConditions() { + filter.setBeginDate(parseDateTimeString("2022-01-10T15:01:44")); + filter.setEndDate(parseDateTimeString("2022-01-15T15:01:44")); + + String sql = jdbcBaseDAO.resolveTimeFilter(filter, UTC_ZONE_ID); + assertThat(sql, is(" WHERE TIME>'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getBeginDate()) + "'" // + + " AND TIME<'" + JdbcBaseDAO.JDBC_DATE_FORMAT.format(filter.getEndDate()) + "'")); + } + + private ZonedDateTime parseDateTimeString(String dts) { + return ZonedDateTime.of(LocalDateTime.parse(dts, DATE_PARSER), UTC_ZONE_ID); + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceTest.java b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceTest.java new file mode 100644 index 000000000..27f4941ed --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceTest.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.internal; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.persistence.FilterCriteria; + +/** + * Tests the {@link JdbcPersistenceService}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class JdbcPersistenceServiceTest { + + private final JdbcPersistenceService jdbcPersistenceService = new JdbcPersistenceService(mock(ItemRegistry.class), + mock(TimeZoneProvider.class)) { + @Override + protected boolean checkDBAccessability() { + return true; + } + }; + private @NonNullByDefault({}) FilterCriteria filter; + + @BeforeEach + public void setup() { + filter = new FilterCriteria(); + } + + @Test + void removeThrowsIllegalArgumentExceptionIfItemNameOfFilterIsNull() { + assertThrows(IllegalArgumentException.class, () -> jdbcPersistenceService.remove(filter)); + } +}