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