]> git.basschouten.com Git - openhab-addons.git/blob
e0fbf11552b607518bb3c733cf99d5ec4c00656e
[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.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;
55
56 /**
57  * This is the implementation of the JDBC {@link PersistenceService}.
58  *
59  * @author Helmut Lehmeyer - Initial contribution
60  * @author Kai Kreuzer - Migration to 3.x
61  */
62 @NonNullByDefault
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 {
68
69     private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
70
71     private final ItemRegistry itemRegistry;
72
73     @Activate
74     public JdbcPersistenceService(final @Reference ItemRegistry itemRegistry,
75             final @Reference TimeZoneProvider timeZoneProvider) {
76         super(timeZoneProvider);
77         this.itemRegistry = itemRegistry;
78     }
79
80     /**
81      * Called by the SCR to activate the component with its configuration read
82      * from CAS
83      *
84      * @param bundleContext
85      *            BundleContext of the Bundle that defines this component
86      * @param configuration
87      *            Configuration properties for this component obtained from the
88      *            ConfigAdmin service
89      */
90     @Activate
91     public void activate(BundleContext bundleContext, Map<Object, Object> configuration) {
92         logger.debug("JDBC::activate: persistence service activated");
93         updateConfig(configuration);
94     }
95
96     /**
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.
100      *
101      * @param reason
102      *            Reason code for the deactivation:<br>
103      *            <ul>
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
111      *            </ul>
112      */
113     @Deactivate
114     public void deactivate(final int reason) {
115         logger.debug("JDBC::deactivate:  persistence bundle stopping. Disconnecting from database. reason={}", reason);
116         // closeConnection();
117         initialized = false;
118     }
119
120     @Override
121     public String getId() {
122         logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
123         return JdbcPersistenceServiceConstants.SERVICE_ID;
124     }
125
126     @Override
127     public String getLabel(@Nullable Locale locale) {
128         return JdbcPersistenceServiceConstants.SERVICE_LABEL;
129     }
130
131     @Override
132     public void store(Item item) {
133         internalStore(item, null, item.getState());
134     }
135
136     @Override
137     public void store(Item item, @Nullable String alias) {
138         // alias is not supported
139         internalStore(item, null, item.getState());
140     }
141
142     @Override
143     public void store(Item item, ZonedDateTime date, State state) {
144         internalStore(item, date, state);
145     }
146
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());
151             return;
152         }
153         if (!checkDBAccessability()) {
154             logger.warn(
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());
157             return;
158         }
159         try {
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);
165             }
166         } catch (YankSQLException e) {
167             logger.warn("JDBC::store: Unable to store item", e);
168         }
169     }
170
171     @Override
172     public Set<PersistenceItemInfo> getItemInfo() {
173         return getItems();
174     }
175
176     /**
177      * Queries the {@link PersistenceService} for data with a given filter
178      * criteria
179      *
180      * @param filter
181      *            the filter to apply to the query
182      * @return a time series of items
183      */
184     @Override
185     public Iterable<HistoricItem> query(FilterCriteria filter) {
186         if (!checkDBAccessability()) {
187             logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
188             return List.of();
189         }
190
191         // Get the item name from the filter
192         // Also get the Item object so we can determine the type
193         Item item = null;
194         String itemName = filter.getItemName();
195         logger.debug("JDBC::query: item is {}", itemName);
196         try {
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);
200             return List.of();
201         }
202
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);
207             if (item == null) {
208                 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
209                 return List.of();
210             }
211             if (item instanceof GroupItem) {
212                 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
213                 return List.of();
214             }
215         }
216
217         String table = itemNameToTableNameMap.get(itemName);
218         if (table == null) {
219             logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
220             return List.of();
221         }
222
223         try {
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);
229             }
230             // Success
231             errCnt = 0;
232             return items;
233         } catch (YankSQLException e) {
234             logger.warn("JDBC::query: Unable to query item", e);
235             return List.of();
236         }
237     }
238
239     public void updateConfig(Map<Object, Object> configuration) {
240         logger.debug("JDBC::updateConfig");
241
242         conf = new JdbcConfiguration(configuration);
243         if (conf.valid && checkDBAccessability()) {
244             namingStrategy = new NamingStrategy(conf);
245             try {
246                 checkDBSchema();
247                 // connection has been established ... initialization completed!
248                 initialized = true;
249             } catch (YankSQLException e) {
250                 logger.error("Failed to check database schema", e);
251                 initialized = false;
252             }
253         } else {
254             initialized = false;
255         }
256
257         logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
258     }
259
260     @Override
261     public List<PersistenceStrategy> getDefaultStrategies() {
262         return List.of(PersistenceStrategy.Globals.CHANGE);
263     }
264
265     @Override
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());
269             return false;
270         }
271
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");
278         }
279
280         String table = itemNameToTableNameMap.get(itemName);
281         if (table == null) {
282             logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
283             return false;
284         }
285
286         try {
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);
292             }
293             return true;
294         } catch (YankSQLException e) {
295             logger.debug("JDBC::remove: Unable to remove values for item", e);
296             return false;
297         }
298     }
299
300     /**
301      * Get a list of names of persisted items.
302      */
303     public Collection<String> getItemNames() {
304         return itemNameToTableNameMap.keySet();
305     }
306
307     /**
308      * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
309      * its condition.
310      *
311      * @return list of {@link ItemTableCheckEntry}
312      */
313     public List<ItemTableCheckEntry> getCheckedEntries() {
314         List<ItemTableCheckEntry> entries = new ArrayList<>();
315
316         if (!checkDBAccessability()) {
317             logger.warn("JDBC::getCheckedEntries: database not connected");
318             return entries;
319         }
320
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);
327         }
328         for (String orphanTable : orphanTables) {
329             entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
330         }
331         return entries;
332     }
333
334     private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
335         boolean itemExists;
336         try {
337             itemRegistry.getItem(itemName);
338             itemExists = true;
339         } catch (ItemNotFoundException e) {
340             itemExists = false;
341         }
342
343         ItemTableCheckEntryStatus status;
344         if (!tableExists) {
345             if (itemExists) {
346                 status = ItemTableCheckEntryStatus.TABLE_MISSING;
347             } else {
348                 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
349             }
350         } else if (itemExists) {
351             status = ItemTableCheckEntryStatus.VALID;
352         } else {
353             status = ItemTableCheckEntryStatus.ITEM_MISSING;
354         }
355         return new ItemTableCheckEntry(itemName, tableName, status);
356     }
357
358     /**
359      * Clean up inconsistent item: Remove from index and drop table.
360      * Tables with any rows are skipped, unless force is set.
361      *
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
365      */
366     public boolean cleanupItem(String itemName, boolean force) {
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      */
382     public boolean cleanupItem(ItemTableCheckEntry entry) {
383         return cleanupItem(entry, false);
384     }
385
386     private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) {
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 }