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.time.ZonedDateTime;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Date;
19 import java.util.List;
20 import java.util.Locale;
22 import java.util.Map.Entry;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.knowm.yank.exceptions.YankSQLException;
29 import org.openhab.core.config.core.ConfigurableService;
30 import org.openhab.core.i18n.TimeZoneProvider;
31 import org.openhab.core.items.GroupItem;
32 import org.openhab.core.items.Item;
33 import org.openhab.core.items.ItemNotFoundException;
34 import org.openhab.core.items.ItemRegistry;
35 import org.openhab.core.persistence.FilterCriteria;
36 import org.openhab.core.persistence.HistoricItem;
37 import org.openhab.core.persistence.ModifiablePersistenceService;
38 import org.openhab.core.persistence.PersistenceItemInfo;
39 import org.openhab.core.persistence.PersistenceService;
40 import org.openhab.core.persistence.QueryablePersistenceService;
41 import org.openhab.core.persistence.strategy.PersistenceStrategy;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.openhab.persistence.jdbc.ItemTableCheckEntry;
45 import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus;
46 import org.openhab.persistence.jdbc.dto.ItemsVO;
47 import org.osgi.framework.BundleContext;
48 import org.osgi.framework.Constants;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Deactivate;
52 import org.osgi.service.component.annotations.Reference;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * This is the implementation of the JDBC {@link PersistenceService}.
59 * @author Helmut Lehmeyer - Initial contribution
60 * @author Kai Kreuzer - Migration to 3.x
63 @Component(service = { PersistenceService.class,
64 QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", //
65 property = Constants.SERVICE_PID + "=org.openhab.jdbc")
66 @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI)
67 public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService {
69 private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
71 private final ItemRegistry itemRegistry;
74 public JdbcPersistenceService(final @Reference ItemRegistry itemRegistry,
75 final @Reference TimeZoneProvider timeZoneProvider) {
76 super(timeZoneProvider);
77 this.itemRegistry = itemRegistry;
81 * Called by the SCR to activate the component with its configuration read
84 * @param bundleContext
85 * BundleContext of the Bundle that defines this component
86 * @param configuration
87 * Configuration properties for this component obtained from the
91 public void activate(BundleContext bundleContext, Map<Object, Object> configuration) {
92 logger.debug("JDBC::activate: persistence service activated");
93 updateConfig(configuration);
97 * Called by the SCR to deactivate the component when either the
98 * configuration is removed or mandatory references are no longer satisfied
99 * or the component has simply been stopped.
102 * Reason code for the deactivation:<br>
104 * <li>0 – Unspecified
105 * <li>1 – The component was disabled
106 * <li>2 – A reference became unsatisfied
107 * <li>3 – A configuration was changed
108 * <li>4 – A configuration was deleted
109 * <li>5 – The component was disposed
110 * <li>6 – The bundle was stopped
114 public void deactivate(final int reason) {
115 logger.debug("JDBC::deactivate: persistence bundle stopping. Disconnecting from database. reason={}", reason);
116 // closeConnection();
121 public String getId() {
122 logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
123 return JdbcPersistenceServiceConstants.SERVICE_ID;
127 public String getLabel(@Nullable Locale locale) {
128 return JdbcPersistenceServiceConstants.SERVICE_LABEL;
132 public void store(Item item) {
133 internalStore(item, null, item.getState());
137 public void store(Item item, @Nullable String alias) {
138 // alias is not supported
139 internalStore(item, null, item.getState());
143 public void store(Item item, ZonedDateTime date, State state) {
144 internalStore(item, date, state);
147 private void internalStore(Item item, @Nullable ZonedDateTime date, State state) {
148 // Do not store undefined/uninitialized data
149 if (state instanceof UnDefType) {
150 logger.debug("JDBC::store: ignore Item '{}' because it is UnDefType", item.getName());
153 if (!checkDBAccessability()) {
155 "JDBC::store: No connection to database. Cannot persist state '{}' for item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}",
156 state, item, errCnt, conf.getErrReconnectThreshold());
160 long timerStart = System.currentTimeMillis();
161 storeItemValue(item, state, date);
162 if (logger.isDebugEnabled()) {
163 logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), state,
164 new Date(), System.currentTimeMillis() - timerStart);
166 } catch (YankSQLException e) {
167 logger.warn("JDBC::store: Unable to store item", e);
172 public Set<PersistenceItemInfo> getItemInfo() {
177 * Queries the {@link PersistenceService} for data with a given filter
181 * the filter to apply to the query
182 * @return a time series of items
185 public Iterable<HistoricItem> query(FilterCriteria filter) {
186 if (!checkDBAccessability()) {
187 logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
191 // Get the item name from the filter
192 // Also get the Item object so we can determine the type
194 String itemName = filter.getItemName();
195 logger.debug("JDBC::query: item is {}", itemName);
197 item = itemRegistry.getItem(itemName);
198 } catch (ItemNotFoundException e1) {
199 logger.error("JDBC::query: unable to get item for itemName: '{}'. Ignore and give up!", itemName);
203 if (item instanceof GroupItem) {
204 // For Group Item is BaseItem needed to get correct Type of Value.
205 item = GroupItem.class.cast(item).getBaseItem();
206 logger.debug("JDBC::query: item is instanceof GroupItem '{}'", itemName);
208 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
211 if (item instanceof GroupItem) {
212 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
217 String table = itemNameToTableNameMap.get(itemName);
219 logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
224 long timerStart = System.currentTimeMillis();
225 List<HistoricItem> items = getHistItemFilterQuery(filter, conf.getNumberDecimalcount(), table, item);
226 if (logger.isDebugEnabled()) {
227 logger.debug("JDBC: Query for item '{}' returned {} rows in {} ms", itemName, items.size(),
228 System.currentTimeMillis() - timerStart);
233 } catch (YankSQLException e) {
234 logger.warn("JDBC::query: Unable to query item", e);
239 public void updateConfig(Map<Object, Object> configuration) {
240 logger.debug("JDBC::updateConfig");
242 conf = new JdbcConfiguration(configuration);
243 if (conf.valid && checkDBAccessability()) {
244 namingStrategy = new NamingStrategy(conf);
247 // connection has been established ... initialization completed!
249 } catch (YankSQLException e) {
250 logger.error("Failed to check database schema", e);
257 logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
261 public List<PersistenceStrategy> getDefaultStrategies() {
262 return List.of(PersistenceStrategy.Globals.CHANGE);
266 public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
267 if (!checkDBAccessability()) {
268 logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName());
272 // Get the item name from the filter
273 // Also get the Item object so we can determine the type
274 String itemName = filter.getItemName();
275 logger.debug("JDBC::remove: item is {}", itemName);
276 if (itemName == null) {
277 throw new IllegalArgumentException("Item name must not be null");
280 String table = itemNameToTableNameMap.get(itemName);
282 logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
287 long timerStart = System.currentTimeMillis();
288 deleteItemValues(filter, table);
289 if (logger.isDebugEnabled()) {
290 logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", itemName, new Date(),
291 System.currentTimeMillis() - timerStart);
294 } catch (YankSQLException e) {
295 logger.debug("JDBC::remove: Unable to remove values for item", e);
301 * Get a list of names of persisted items.
303 public Collection<String> getItemNames() {
304 return itemNameToTableNameMap.keySet();
308 * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
311 * @return list of {@link ItemTableCheckEntry}
313 public List<ItemTableCheckEntry> getCheckedEntries() {
314 List<ItemTableCheckEntry> entries = new ArrayList<>();
316 if (!checkDBAccessability()) {
317 logger.warn("JDBC::getCheckedEntries: database not connected");
321 var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
322 for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
323 String itemName = entry.getKey();
324 String tableName = entry.getValue();
325 entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
326 orphanTables.remove(tableName);
328 for (String orphanTable : orphanTables) {
329 entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
334 private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
337 itemRegistry.getItem(itemName);
339 } catch (ItemNotFoundException e) {
343 ItemTableCheckEntryStatus status;
346 status = ItemTableCheckEntryStatus.TABLE_MISSING;
348 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
350 } else if (itemExists) {
351 status = ItemTableCheckEntryStatus.VALID;
353 status = ItemTableCheckEntryStatus.ITEM_MISSING;
355 return new ItemTableCheckEntry(itemName, tableName, status);
359 * Clean up inconsistent item: Remove from index and drop table.
360 * Tables with any rows are skipped, unless force is set.
362 * @param itemName Name of item to clean
363 * @param force If true, non-empty tables will be dropped too
364 * @return true if item was cleaned up
366 public boolean cleanupItem(String itemName, boolean force) {
367 String tableName = itemNameToTableNameMap.get(itemName);
368 if (tableName == null) {
371 ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
372 return cleanupItem(entry, force);
376 * Clean up inconsistent item: Remove from index and drop table.
377 * Tables with any rows are skipped.
380 * @return true if item was cleaned up
382 public boolean cleanupItem(ItemTableCheckEntry entry) {
383 return cleanupItem(entry, false);
386 private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) {
387 if (!checkDBAccessability()) {
388 logger.warn("JDBC::cleanupItem: database not connected");
392 ItemTableCheckEntryStatus status = entry.getStatus();
393 String tableName = entry.getTableName();
396 if (!force && getRowCount(tableName) > 0) {
399 dropTable(tableName);
400 // Fall through to remove from index.
402 case ITEM_AND_TABLE_MISSING:
403 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
404 ItemsVO itemsVo = new ItemsVO();
405 itemsVo.setItemName(entry.getItemName());
406 deleteItemsEntry(itemsVo);
408 itemNameToTableNameMap.remove(entry.getItemName());