]> git.basschouten.com Git - openhab-addons.git/blob
a75f6d2c8af22510eaf70ef89d156dc14f621b28
[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.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;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Set;
24 import java.util.stream.Collectors;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.config.core.ConfigurableService;
29 import org.openhab.core.i18n.TimeZoneProvider;
30 import org.openhab.core.items.GroupItem;
31 import org.openhab.core.items.Item;
32 import org.openhab.core.items.ItemNotFoundException;
33 import org.openhab.core.items.ItemRegistry;
34 import org.openhab.core.persistence.FilterCriteria;
35 import org.openhab.core.persistence.HistoricItem;
36 import org.openhab.core.persistence.ModifiablePersistenceService;
37 import org.openhab.core.persistence.PersistenceItemInfo;
38 import org.openhab.core.persistence.PersistenceService;
39 import org.openhab.core.persistence.QueryablePersistenceService;
40 import org.openhab.core.persistence.strategy.PersistenceStrategy;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
44 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
45 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
46 import org.osgi.framework.BundleContext;
47 import org.osgi.framework.Constants;
48 import org.osgi.service.component.annotations.Activate;
49 import org.osgi.service.component.annotations.Component;
50 import org.osgi.service.component.annotations.Deactivate;
51 import org.osgi.service.component.annotations.Reference;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * This is the implementation of the JDBC {@link PersistenceService}.
57  *
58  * @author Helmut Lehmeyer - Initial contribution
59  * @author Kai Kreuzer - Migration to 3.x
60  */
61 @NonNullByDefault
62 @Component(service = { PersistenceService.class,
63         QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", //
64         property = Constants.SERVICE_PID + "=org.openhab.jdbc")
65 @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI)
66 public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService {
67
68     private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
69
70     private final ItemRegistry itemRegistry;
71
72     @Activate
73     public JdbcPersistenceService(final @Reference ItemRegistry itemRegistry,
74             final @Reference TimeZoneProvider timeZoneProvider) {
75         super(timeZoneProvider);
76         this.itemRegistry = itemRegistry;
77     }
78
79     /**
80      * Called by the SCR to activate the component with its configuration read
81      * from CAS
82      *
83      * @param bundleContext
84      *            BundleContext of the Bundle that defines this component
85      * @param configuration
86      *            Configuration properties for this component obtained from the
87      *            ConfigAdmin service
88      */
89     @Activate
90     public void activate(BundleContext bundleContext, Map<Object, Object> configuration) {
91         logger.debug("JDBC::activate: persistence service activated");
92         updateConfig(configuration);
93     }
94
95     /**
96      * Called by the SCR to deactivate the component when either the
97      * configuration is removed or mandatory references are no longer satisfied
98      * or the component has simply been stopped.
99      *
100      * @param reason
101      *            Reason code for the deactivation:<br>
102      *            <ul>
103      *            <li>0 – Unspecified
104      *            <li>1 – The component was disabled
105      *            <li>2 – A reference became unsatisfied
106      *            <li>3 – A configuration was changed
107      *            <li>4 – A configuration was deleted
108      *            <li>5 – The component was disposed
109      *            <li>6 – The bundle was stopped
110      *            </ul>
111      */
112     @Deactivate
113     public void deactivate(final int reason) {
114         logger.debug("JDBC::deactivate:  persistence bundle stopping. Disconnecting from database. reason={}", reason);
115         // closeConnection();
116         initialized = false;
117     }
118
119     @Override
120     public String getId() {
121         logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
122         return JdbcPersistenceServiceConstants.SERVICE_ID;
123     }
124
125     @Override
126     public String getLabel(@Nullable Locale locale) {
127         return JdbcPersistenceServiceConstants.SERVICE_LABEL;
128     }
129
130     @Override
131     public void store(Item item) {
132         internalStore(item, null, item.getState());
133     }
134
135     @Override
136     public void store(Item item, @Nullable String alias) {
137         // alias is not supported
138         internalStore(item, null, item.getState());
139     }
140
141     @Override
142     public void store(Item item, ZonedDateTime date, State state) {
143         internalStore(item, date, state);
144     }
145
146     private void internalStore(Item item, @Nullable ZonedDateTime date, State state) {
147         // Do not store undefined/uninitialized data
148         if (state instanceof UnDefType) {
149             logger.debug("JDBC::store: ignore Item '{}' because it is UnDefType", item.getName());
150             return;
151         }
152         if (!checkDBAccessability()) {
153             logger.warn(
154                     "JDBC::store: No connection to database. Cannot persist state '{}' for item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}",
155                     state, item, errCnt, conf.getErrReconnectThreshold());
156             return;
157         }
158         try {
159             long timerStart = System.currentTimeMillis();
160             storeItemValue(item, state, date);
161             if (logger.isDebugEnabled()) {
162                 logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), state,
163                         new Date(), System.currentTimeMillis() - timerStart);
164             }
165         } catch (JdbcException e) {
166             logger.warn("JDBC::store: Unable to store item", e);
167         }
168     }
169
170     @Override
171     public Set<PersistenceItemInfo> getItemInfo() {
172         return getItems();
173     }
174
175     /**
176      * Queries the {@link PersistenceService} for data with a given filter
177      * criteria
178      *
179      * @param filter
180      *            the filter to apply to the query
181      * @return a time series of items
182      */
183     @Override
184     public Iterable<HistoricItem> query(FilterCriteria filter) {
185         if (!checkDBAccessability()) {
186             logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
187             return List.of();
188         }
189
190         // Get the item name from the filter
191         // Also get the Item object so we can determine the type
192         Item item = null;
193         String itemName = filter.getItemName();
194         logger.debug("JDBC::query: item is {}", itemName);
195         try {
196             item = itemRegistry.getItem(itemName);
197         } catch (ItemNotFoundException e1) {
198             logger.error("JDBC::query: unable to get item for itemName: '{}'. Ignore and give up!", itemName);
199             return List.of();
200         }
201
202         if (item instanceof GroupItem) {
203             // For Group Item is BaseItem needed to get correct Type of Value.
204             item = GroupItem.class.cast(item).getBaseItem();
205             logger.debug("JDBC::query: item is instanceof GroupItem '{}'", itemName);
206             if (item == null) {
207                 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
208                 return List.of();
209             }
210             if (item instanceof GroupItem) {
211                 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
212                 return List.of();
213             }
214         }
215
216         String table = itemNameToTableNameMap.get(itemName);
217         if (table == null) {
218             logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
219             return List.of();
220         }
221
222         try {
223             long timerStart = System.currentTimeMillis();
224             List<HistoricItem> items = getHistItemFilterQuery(filter, conf.getNumberDecimalcount(), table, item);
225             if (logger.isDebugEnabled()) {
226                 logger.debug("JDBC: Query for item '{}' returned {} rows in {} ms", itemName, items.size(),
227                         System.currentTimeMillis() - timerStart);
228             }
229             // Success
230             errCnt = 0;
231             return items;
232         } catch (JdbcSQLException e) {
233             logger.warn("JDBC::query: Unable to query item", e);
234             return List.of();
235         }
236     }
237
238     public void updateConfig(Map<Object, Object> configuration) {
239         logger.debug("JDBC::updateConfig");
240
241         conf = new JdbcConfiguration(configuration);
242         if (conf.valid && checkDBAccessability()) {
243             namingStrategy = new NamingStrategy(conf);
244             try {
245                 checkDBSchema();
246                 // connection has been established ... initialization completed!
247                 initialized = true;
248             } catch (JdbcSQLException e) {
249                 logger.error("Failed to check database schema", e);
250                 initialized = false;
251             }
252         } else {
253             initialized = false;
254         }
255
256         logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
257     }
258
259     @Override
260     public List<PersistenceStrategy> getDefaultStrategies() {
261         return List.of(PersistenceStrategy.Globals.CHANGE);
262     }
263
264     @Override
265     public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
266         if (!checkDBAccessability()) {
267             logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName());
268             return false;
269         }
270
271         // Get the item name from the filter
272         // Also get the Item object so we can determine the type
273         String itemName = filter.getItemName();
274         logger.debug("JDBC::remove: item is {}", itemName);
275         if (itemName == null) {
276             throw new IllegalArgumentException("Item name must not be null");
277         }
278
279         String table = itemNameToTableNameMap.get(itemName);
280         if (table == null) {
281             logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
282             return false;
283         }
284
285         try {
286             long timerStart = System.currentTimeMillis();
287             deleteItemValues(filter, table);
288             if (logger.isDebugEnabled()) {
289                 logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", itemName, new Date(),
290                         System.currentTimeMillis() - timerStart);
291             }
292             return true;
293         } catch (JdbcSQLException e) {
294             logger.debug("JDBC::remove: Unable to remove values for item", e);
295             return false;
296         }
297     }
298
299     /**
300      * Get a list of names of persisted items.
301      */
302     public Collection<String> getItemNames() {
303         return itemNameToTableNameMap.keySet();
304     }
305
306     /**
307      * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
308      * its condition.
309      *
310      * @return list of {@link ItemTableCheckEntry}
311      */
312     public List<ItemTableCheckEntry> getCheckedEntries() throws JdbcSQLException {
313         List<ItemTableCheckEntry> entries = new ArrayList<>();
314
315         if (!checkDBAccessability()) {
316             logger.warn("JDBC::getCheckedEntries: database not connected");
317             return entries;
318         }
319
320         var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
321         for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
322             String itemName = entry.getKey();
323             String tableName = entry.getValue();
324             entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
325             orphanTables.remove(tableName);
326         }
327         for (String orphanTable : orphanTables) {
328             entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
329         }
330         return entries;
331     }
332
333     private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
334         boolean itemExists;
335         try {
336             itemRegistry.getItem(itemName);
337             itemExists = true;
338         } catch (ItemNotFoundException e) {
339             itemExists = false;
340         }
341
342         ItemTableCheckEntryStatus status;
343         if (!tableExists) {
344             if (itemExists) {
345                 status = ItemTableCheckEntryStatus.TABLE_MISSING;
346             } else {
347                 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
348             }
349         } else if (itemExists) {
350             status = ItemTableCheckEntryStatus.VALID;
351         } else {
352             status = ItemTableCheckEntryStatus.ITEM_MISSING;
353         }
354         return new ItemTableCheckEntry(itemName, tableName, status);
355     }
356
357     /**
358      * Clean up inconsistent item: Remove from index and drop table.
359      * Tables with any rows are skipped, unless force is set.
360      *
361      * @param itemName Name of item to clean
362      * @param force If true, non-empty tables will be dropped too
363      * @return true if item was cleaned up
364      * @throws JdbcSQLException
365      */
366     public boolean cleanupItem(String itemName, boolean force) throws JdbcSQLException {
367         String tableName = itemNameToTableNameMap.get(itemName);
368         if (tableName == null) {
369             return false;
370         }
371         ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
372         return cleanupItem(entry, force);
373     }
374
375     /**
376      * Clean up inconsistent item: Remove from index and drop table.
377      * Tables with any rows are skipped.
378      *
379      * @param entry
380      * @return true if item was cleaned up
381      * @throws JdbcSQLException
382      */
383     public boolean cleanupItem(ItemTableCheckEntry entry) throws JdbcSQLException {
384         return cleanupItem(entry, false);
385     }
386
387     private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) throws JdbcSQLException {
388         if (!checkDBAccessability()) {
389             logger.warn("JDBC::cleanupItem: database not connected");
390             return false;
391         }
392
393         ItemTableCheckEntryStatus status = entry.getStatus();
394         String tableName = entry.getTableName();
395         switch (status) {
396             case ITEM_MISSING:
397                 if (!force && getRowCount(tableName) > 0) {
398                     return false;
399                 }
400                 dropTable(tableName);
401                 // Fall through to remove from index.
402             case TABLE_MISSING:
403             case ITEM_AND_TABLE_MISSING:
404                 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
405                     ItemsVO itemsVo = new ItemsVO();
406                     itemsVo.setItemName(entry.getItemName());
407                     deleteItemsEntry(itemsVo);
408                 }
409                 itemNameToTableNameMap.remove(entry.getItemName());
410                 return true;
411             case ORPHAN_TABLE:
412             case VALID:
413             default:
414                 // Nothing to clean.
415                 return false;
416         }
417     }
418 }