]> git.basschouten.com Git - openhab-addons.git/blob
d267ad097045dc11b6f4385ff19bd7e908dd7f87
[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.ItemTableCheckEntry;
44 import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus;
45 import org.openhab.persistence.jdbc.dto.ItemsVO;
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         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     }
165
166     @Override
167     public Set<PersistenceItemInfo> getItemInfo() {
168         return getItems();
169     }
170
171     /**
172      * Queries the {@link PersistenceService} for data with a given filter
173      * criteria
174      *
175      * @param filter
176      *            the filter to apply to the query
177      * @return a time series of items
178      */
179     @Override
180     public Iterable<HistoricItem> query(FilterCriteria filter) {
181         if (!checkDBAccessability()) {
182             logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
183             return List.of();
184         }
185
186         // Get the item name from the filter
187         // Also get the Item object so we can determine the type
188         Item item = null;
189         String itemName = filter.getItemName();
190         logger.debug("JDBC::query: item is {}", itemName);
191         try {
192             item = itemRegistry.getItem(itemName);
193         } catch (ItemNotFoundException e1) {
194             logger.error("JDBC::query: unable to get item for itemName: '{}'. Ignore and give up!", itemName);
195             return List.of();
196         }
197
198         if (item instanceof GroupItem) {
199             // For Group Item is BaseItem needed to get correct Type of Value.
200             item = GroupItem.class.cast(item).getBaseItem();
201             logger.debug("JDBC::query: item is instanceof GroupItem '{}'", itemName);
202             if (item == null) {
203                 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
204                 return List.of();
205             }
206             if (item instanceof GroupItem) {
207                 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
208                 return List.of();
209             }
210         }
211
212         String table = itemNameToTableNameMap.get(itemName);
213         if (table == null) {
214             logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
215             return List.of();
216         }
217
218         long timerStart = System.currentTimeMillis();
219         List<HistoricItem> items = getHistItemFilterQuery(filter, conf.getNumberDecimalcount(), table, item);
220         if (logger.isDebugEnabled()) {
221             logger.debug("JDBC: Query for item '{}' returned {} rows in {} ms", itemName, items.size(),
222                     System.currentTimeMillis() - timerStart);
223         }
224
225         // Success
226         errCnt = 0;
227         return items;
228     }
229
230     public void updateConfig(Map<Object, Object> configuration) {
231         logger.debug("JDBC::updateConfig");
232
233         conf = new JdbcConfiguration(configuration);
234         if (conf.valid && checkDBAccessability()) {
235             namingStrategy = new NamingStrategy(conf);
236             checkDBSchema();
237             // connection has been established ... initialization completed!
238             initialized = true;
239         } else {
240             initialized = false;
241         }
242
243         logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
244     }
245
246     @Override
247     public List<PersistenceStrategy> getDefaultStrategies() {
248         return List.of(PersistenceStrategy.Globals.CHANGE);
249     }
250
251     @Override
252     public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
253         if (!checkDBAccessability()) {
254             logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName());
255             return false;
256         }
257
258         // Get the item name from the filter
259         // Also get the Item object so we can determine the type
260         String itemName = filter.getItemName();
261         logger.debug("JDBC::remove: item is {}", itemName);
262         if (itemName == null) {
263             throw new IllegalArgumentException("Item name must not be null");
264         }
265
266         String table = itemNameToTableNameMap.get(itemName);
267         if (table == null) {
268             logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
269             return false;
270         }
271
272         long timerStart = System.currentTimeMillis();
273         boolean result = deleteItemValues(filter, table);
274         if (logger.isDebugEnabled()) {
275             logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", itemName, new Date(),
276                     System.currentTimeMillis() - timerStart);
277         }
278
279         return result;
280     }
281
282     /**
283      * Get a list of names of persisted items.
284      */
285     public Collection<String> getItemNames() {
286         return itemNameToTableNameMap.keySet();
287     }
288
289     /**
290      * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
291      * its condition.
292      *
293      * @return list of {@link ItemTableCheckEntry}
294      */
295     public List<ItemTableCheckEntry> getCheckedEntries() {
296         List<ItemTableCheckEntry> entries = new ArrayList<>();
297
298         if (!checkDBAccessability()) {
299             logger.warn("JDBC::getCheckedEntries: database not connected");
300             return entries;
301         }
302
303         var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
304         for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
305             String itemName = entry.getKey();
306             String tableName = entry.getValue();
307             entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
308             orphanTables.remove(tableName);
309         }
310         for (String orphanTable : orphanTables) {
311             entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
312         }
313         return entries;
314     }
315
316     private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
317         boolean itemExists;
318         try {
319             itemRegistry.getItem(itemName);
320             itemExists = true;
321         } catch (ItemNotFoundException e) {
322             itemExists = false;
323         }
324
325         ItemTableCheckEntryStatus status;
326         if (!tableExists) {
327             if (itemExists) {
328                 status = ItemTableCheckEntryStatus.TABLE_MISSING;
329             } else {
330                 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
331             }
332         } else if (itemExists) {
333             status = ItemTableCheckEntryStatus.VALID;
334         } else {
335             status = ItemTableCheckEntryStatus.ITEM_MISSING;
336         }
337         return new ItemTableCheckEntry(itemName, tableName, status);
338     }
339
340     /**
341      * Clean up inconsistent item: Remove from index and drop table.
342      * Tables with any rows are skipped, unless force is set.
343      *
344      * @param itemName Name of item to clean
345      * @param force If true, non-empty tables will be dropped too
346      * @return true if item was cleaned up
347      */
348     public boolean cleanupItem(String itemName, boolean force) {
349         String tableName = itemNameToTableNameMap.get(itemName);
350         if (tableName == null) {
351             return false;
352         }
353         ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
354         return cleanupItem(entry, force);
355     }
356
357     /**
358      * Clean up inconsistent item: Remove from index and drop table.
359      * Tables with any rows are skipped.
360      *
361      * @param entry
362      * @return true if item was cleaned up
363      */
364     public boolean cleanupItem(ItemTableCheckEntry entry) {
365         return cleanupItem(entry, false);
366     }
367
368     private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) {
369         if (!checkDBAccessability()) {
370             logger.warn("JDBC::cleanupItem: database not connected");
371             return false;
372         }
373
374         ItemTableCheckEntryStatus status = entry.getStatus();
375         String tableName = entry.getTableName();
376         switch (status) {
377             case ITEM_MISSING:
378                 if (!force && getRowCount(tableName) > 0) {
379                     return false;
380                 }
381                 dropTable(tableName);
382                 // Fall through to remove from index.
383             case TABLE_MISSING:
384             case ITEM_AND_TABLE_MISSING:
385                 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
386                     ItemsVO itemsVo = new ItemsVO();
387                     itemsVo.setItemName(entry.getItemName());
388                     deleteItemsEntry(itemsVo);
389                 }
390                 itemNameToTableNameMap.remove(entry.getItemName());
391                 return true;
392             case ORPHAN_TABLE:
393             case VALID:
394             default:
395                 // Nothing to clean.
396                 return false;
397         }
398     }
399 }