]> git.basschouten.com Git - openhab-addons.git/blob
6ba41ba039cd2f7dcabcfa2e9dae3e0c3d812cac
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.db.JdbcBaseDAO;
44 import org.openhab.persistence.jdbc.internal.dto.Column;
45 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
46 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
47 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
48 import org.osgi.framework.BundleContext;
49 import org.osgi.framework.Constants;
50 import org.osgi.service.component.annotations.Activate;
51 import org.osgi.service.component.annotations.Component;
52 import org.osgi.service.component.annotations.Deactivate;
53 import org.osgi.service.component.annotations.Reference;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * This is the implementation of the JDBC {@link PersistenceService}.
59  *
60  * @author Helmut Lehmeyer - Initial contribution
61  * @author Kai Kreuzer - Migration to 3.x
62  */
63 @NonNullByDefault
64 @Component(service = { PersistenceService.class,
65         QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", //
66         property = Constants.SERVICE_PID + "=org.openhab.jdbc")
67 @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI)
68 public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService {
69
70     private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
71
72     private final ItemRegistry itemRegistry;
73
74     @Activate
75     public JdbcPersistenceService(final @Reference ItemRegistry itemRegistry,
76             final @Reference TimeZoneProvider timeZoneProvider) {
77         super(timeZoneProvider);
78         this.itemRegistry = itemRegistry;
79     }
80
81     /**
82      * Called by the SCR to activate the component with its configuration read
83      * from CAS
84      *
85      * @param bundleContext
86      *            BundleContext of the Bundle that defines this component
87      * @param configuration
88      *            Configuration properties for this component obtained from the
89      *            ConfigAdmin service
90      */
91     @Activate
92     public void activate(BundleContext bundleContext, Map<Object, Object> configuration) {
93         logger.debug("JDBC::activate: persistence service activated");
94         updateConfig(configuration);
95     }
96
97     /**
98      * Called by the SCR to deactivate the component when either the
99      * configuration is removed or mandatory references are no longer satisfied
100      * or the component has simply been stopped.
101      *
102      * @param reason
103      *            Reason code for the deactivation:<br>
104      *            <ul>
105      *            <li>0 – Unspecified
106      *            <li>1 – The component was disabled
107      *            <li>2 – A reference became unsatisfied
108      *            <li>3 – A configuration was changed
109      *            <li>4 – A configuration was deleted
110      *            <li>5 – The component was disposed
111      *            <li>6 – The bundle was stopped
112      *            </ul>
113      */
114     @Deactivate
115     public void deactivate(final int reason) {
116         logger.debug("JDBC::deactivate:  persistence bundle stopping. Disconnecting from database. reason={}", reason);
117         // closeConnection();
118         initialized = false;
119     }
120
121     @Override
122     public String getId() {
123         logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
124         return JdbcPersistenceServiceConstants.SERVICE_ID;
125     }
126
127     @Override
128     public String getLabel(@Nullable Locale locale) {
129         return JdbcPersistenceServiceConstants.SERVICE_LABEL;
130     }
131
132     @Override
133     public void store(Item item) {
134         internalStore(item, null, item.getState());
135     }
136
137     @Override
138     public void store(Item item, @Nullable String alias) {
139         // alias is not supported
140         internalStore(item, null, item.getState());
141     }
142
143     @Override
144     public void store(Item item, ZonedDateTime date, State state) {
145         internalStore(item, date, state);
146     }
147
148     private void internalStore(Item item, @Nullable ZonedDateTime date, State state) {
149         // Do not store undefined/uninitialized data
150         if (state instanceof UnDefType) {
151             logger.debug("JDBC::store: ignore Item '{}' because it is UnDefType", item.getName());
152             return;
153         }
154         if (!checkDBAccessability()) {
155             logger.warn(
156                     "JDBC::store: No connection to database. Cannot persist state '{}' for item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}",
157                     state, item, errCnt, conf.getErrReconnectThreshold());
158             return;
159         }
160         try {
161             long timerStart = System.currentTimeMillis();
162             storeItemValue(item, state, date);
163             if (logger.isDebugEnabled()) {
164                 logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), state,
165                         new Date(), System.currentTimeMillis() - timerStart);
166             }
167         } catch (JdbcException e) {
168             logger.warn("JDBC::store: Unable to store item", e);
169         }
170     }
171
172     @Override
173     public Set<PersistenceItemInfo> getItemInfo() {
174         return getItems();
175     }
176
177     /**
178      * Queries the {@link PersistenceService} for data with a given filter
179      * criteria
180      *
181      * @param filter
182      *            the filter to apply to the query
183      * @return a time series of items
184      */
185     @Override
186     public Iterable<HistoricItem> query(FilterCriteria filter) {
187         if (!checkDBAccessability()) {
188             logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
189             return List.of();
190         }
191
192         // Get the item name from the filter
193         // Also get the Item object so we can determine the type
194         Item item = null;
195         String itemName = filter.getItemName();
196         if (itemName == null) {
197             logger.warn("Item name is missing in filter {}", filter);
198             return List.of();
199         }
200         logger.debug("JDBC::query: item is {}", itemName);
201         try {
202             item = itemRegistry.getItem(itemName);
203         } catch (ItemNotFoundException e1) {
204             logger.error("JDBC::query: unable to get item for itemName: '{}'. Ignore and give up!", itemName);
205             return List.of();
206         }
207
208         if (item instanceof GroupItem) {
209             // For Group Item is BaseItem needed to get correct Type of Value.
210             item = GroupItem.class.cast(item).getBaseItem();
211             logger.debug("JDBC::query: item is instanceof GroupItem '{}'", itemName);
212             if (item == null) {
213                 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
214                 return List.of();
215             }
216             if (item instanceof GroupItem) {
217                 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
218                 return List.of();
219             }
220         }
221
222         String table = itemNameToTableNameMap.get(itemName);
223         if (table == null) {
224             logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
225             return List.of();
226         }
227
228         try {
229             long timerStart = System.currentTimeMillis();
230             List<HistoricItem> items = getHistItemFilterQuery(filter, conf.getNumberDecimalcount(), table, item);
231             if (logger.isDebugEnabled()) {
232                 logger.debug("JDBC: Query for item '{}' returned {} rows in {} ms", itemName, items.size(),
233                         System.currentTimeMillis() - timerStart);
234             }
235             // Success
236             errCnt = 0;
237             return items;
238         } catch (JdbcSQLException e) {
239             logger.warn("JDBC::query: Unable to query item", e);
240             return List.of();
241         }
242     }
243
244     public void updateConfig(Map<Object, Object> configuration) {
245         logger.debug("JDBC::updateConfig");
246
247         conf = new JdbcConfiguration(configuration);
248         if (conf.valid && checkDBAccessability()) {
249             namingStrategy = new NamingStrategy(conf);
250             try {
251                 checkDBSchema();
252                 // connection has been established ... initialization completed!
253                 initialized = true;
254             } catch (JdbcSQLException e) {
255                 logger.error("Failed to check database schema", e);
256                 initialized = false;
257             }
258         } else {
259             initialized = false;
260         }
261
262         logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
263     }
264
265     @Override
266     public List<PersistenceStrategy> getDefaultStrategies() {
267         return List.of(PersistenceStrategy.Globals.CHANGE);
268     }
269
270     @Override
271     public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
272         if (!checkDBAccessability()) {
273             logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName());
274             return false;
275         }
276
277         // Get the item name from the filter
278         // Also get the Item object so we can determine the type
279         String itemName = filter.getItemName();
280         logger.debug("JDBC::remove: item is {}", itemName);
281         if (itemName == null) {
282             throw new IllegalArgumentException("Item name must not be null");
283         }
284
285         String table = itemNameToTableNameMap.get(itemName);
286         if (table == null) {
287             logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
288             return false;
289         }
290
291         try {
292             long timerStart = System.currentTimeMillis();
293             deleteItemValues(filter, table);
294             if (logger.isDebugEnabled()) {
295                 logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", itemName, new Date(),
296                         System.currentTimeMillis() - timerStart);
297             }
298             return true;
299         } catch (JdbcSQLException e) {
300             logger.debug("JDBC::remove: Unable to remove values for item", e);
301             return false;
302         }
303     }
304
305     /**
306      * Get a list of names of persisted items.
307      */
308     public Collection<String> getItemNames() {
309         return itemNameToTableNameMap.keySet();
310     }
311
312     /**
313      * Get a map of item names to table names.
314      */
315     public Map<String, String> getItemNameToTableNameMap() {
316         return itemNameToTableNameMap;
317     }
318
319     /**
320      * Check schema of specific item table for integrity issues.
321      *
322      * @param tableName for which columns should be checked
323      * @param itemName that corresponds to table
324      * @return Collection of strings, each describing an identified issue
325      * @throws JdbcSQLException on SQL errors
326      */
327     public Collection<String> getSchemaIssues(String tableName, String itemName) throws JdbcSQLException {
328         List<String> issues = new ArrayList<>();
329
330         if (!checkDBAccessability()) {
331             logger.warn("JDBC::getSchemaIssues: database not connected");
332             return issues;
333         }
334
335         Item item;
336         try {
337             item = itemRegistry.getItem(itemName);
338         } catch (ItemNotFoundException e) {
339             return issues;
340         }
341         JdbcBaseDAO dao = conf.getDBDAO();
342         String timeDataType = dao.sqlTypes.get("tablePrimaryKey");
343         if (timeDataType == null) {
344             return issues;
345         }
346         String valueDataType = dao.getDataType(item);
347         List<Column> columns = getTableColumns(tableName);
348         for (Column column : columns) {
349             String columnName = column.getColumnName();
350             if ("time".equalsIgnoreCase(columnName)) {
351                 if (!"time".equals(columnName)) {
352                     issues.add("Column name 'time' expected, but is '" + columnName + "'");
353                 }
354                 if (!timeDataType.equalsIgnoreCase(column.getColumnType())
355                         && !timeDataType.equalsIgnoreCase(column.getColumnTypeAlias())) {
356                     issues.add("Column type '" + timeDataType + "' expected, but is '"
357                             + column.getColumnType().toUpperCase() + "'");
358                 }
359                 if (column.getIsNullable()) {
360                     issues.add("Column 'time' expected to be NOT NULL, but is nullable");
361                 }
362             } else if ("value".equalsIgnoreCase(columnName)) {
363                 if (!"value".equals(columnName)) {
364                     issues.add("Column name 'value' expected, but is '" + columnName + "'");
365                 }
366                 if (!valueDataType.equalsIgnoreCase(column.getColumnType())
367                         && !valueDataType.equalsIgnoreCase(column.getColumnTypeAlias())) {
368                     issues.add("Column type '" + valueDataType + "' expected, but is '"
369                             + column.getColumnType().toUpperCase() + "'");
370                 }
371                 if (!column.getIsNullable()) {
372                     issues.add("Column 'value' expected to be nullable, but is NOT NULL");
373                 }
374             } else {
375                 issues.add("Column '" + columnName + "' not expected");
376             }
377         }
378         return issues;
379     }
380
381     /**
382      * Fix schema issues.
383      *
384      * @param tableName for which columns should be repaired
385      * @param itemName that corresponds to table
386      * @return true if table was altered, otherwise false
387      * @throws JdbcSQLException on SQL errors
388      */
389     public boolean fixSchemaIssues(String tableName, String itemName) throws JdbcSQLException {
390         if (!checkDBAccessability()) {
391             logger.warn("JDBC::fixSchemaIssues: database not connected");
392             return false;
393         }
394
395         Item item;
396         try {
397             item = itemRegistry.getItem(itemName);
398         } catch (ItemNotFoundException e) {
399             return false;
400         }
401         JdbcBaseDAO dao = conf.getDBDAO();
402         String timeDataType = dao.sqlTypes.get("tablePrimaryKey");
403         if (timeDataType == null) {
404             return false;
405         }
406         String valueDataType = dao.getDataType(item);
407         List<Column> columns = getTableColumns(tableName);
408         boolean isFixed = false;
409         for (Column column : columns) {
410             String columnName = column.getColumnName();
411             if ("time".equalsIgnoreCase(columnName)) {
412                 if (!"time".equals(columnName)
413                         || (!timeDataType.equalsIgnoreCase(column.getColumnType())
414                                 && !timeDataType.equalsIgnoreCase(column.getColumnTypeAlias()))
415                         || column.getIsNullable()) {
416                     alterTableColumn(tableName, "time", timeDataType, false);
417                     isFixed = true;
418                 }
419             } else if ("value".equalsIgnoreCase(columnName)) {
420                 if (!"value".equals(columnName)
421                         || (!valueDataType.equalsIgnoreCase(column.getColumnType())
422                                 && !valueDataType.equalsIgnoreCase(column.getColumnTypeAlias()))
423                         || !column.getIsNullable()) {
424                     alterTableColumn(tableName, "value", valueDataType, true);
425                     isFixed = true;
426                 }
427             }
428         }
429         return isFixed;
430     }
431
432     /**
433      * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
434      * its condition.
435      *
436      * @return list of {@link ItemTableCheckEntry}
437      */
438     public List<ItemTableCheckEntry> getCheckedEntries() throws JdbcSQLException {
439         List<ItemTableCheckEntry> entries = new ArrayList<>();
440
441         if (!checkDBAccessability()) {
442             logger.warn("JDBC::getCheckedEntries: database not connected");
443             return entries;
444         }
445
446         var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
447         for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
448             String itemName = entry.getKey();
449             String tableName = entry.getValue();
450             entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
451             orphanTables.remove(tableName);
452         }
453         for (String orphanTable : orphanTables) {
454             entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
455         }
456         return entries;
457     }
458
459     private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
460         boolean itemExists;
461         try {
462             itemRegistry.getItem(itemName);
463             itemExists = true;
464         } catch (ItemNotFoundException e) {
465             itemExists = false;
466         }
467
468         ItemTableCheckEntryStatus status;
469         if (!tableExists) {
470             if (itemExists) {
471                 status = ItemTableCheckEntryStatus.TABLE_MISSING;
472             } else {
473                 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
474             }
475         } else if (itemExists) {
476             status = ItemTableCheckEntryStatus.VALID;
477         } else {
478             status = ItemTableCheckEntryStatus.ITEM_MISSING;
479         }
480         return new ItemTableCheckEntry(itemName, tableName, status);
481     }
482
483     /**
484      * Clean up inconsistent item: Remove from index and drop table.
485      * Tables with any rows are skipped, unless force is set.
486      *
487      * @param itemName Name of item to clean
488      * @param force If true, non-empty tables will be dropped too
489      * @return true if item was cleaned up
490      * @throws JdbcSQLException
491      */
492     public boolean cleanupItem(String itemName, boolean force) throws JdbcSQLException {
493         if (!checkDBAccessability()) {
494             logger.warn("JDBC::cleanupItem: database not connected");
495             return false;
496         }
497
498         String tableName = itemNameToTableNameMap.get(itemName);
499         if (tableName == null) {
500             return false;
501         }
502         ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
503         return cleanupItem(entry, force);
504     }
505
506     /**
507      * Clean up inconsistent item: Remove from index and drop table.
508      * Tables with any rows are skipped.
509      *
510      * @param entry
511      * @return true if item was cleaned up
512      * @throws JdbcSQLException
513      */
514     public boolean cleanupItem(ItemTableCheckEntry entry) throws JdbcSQLException {
515         return cleanupItem(entry, false);
516     }
517
518     private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) throws JdbcSQLException {
519         if (!checkDBAccessability()) {
520             logger.warn("JDBC::cleanupItem: database not connected");
521             return false;
522         }
523
524         ItemTableCheckEntryStatus status = entry.getStatus();
525         String tableName = entry.getTableName();
526         switch (status) {
527             case ITEM_MISSING:
528                 if (!force && getRowCount(tableName) > 0) {
529                     return false;
530                 }
531                 dropTable(tableName);
532                 // Fall through to remove from index.
533             case TABLE_MISSING:
534             case ITEM_AND_TABLE_MISSING:
535                 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
536                     ItemsVO itemsVo = new ItemsVO();
537                     itemsVo.setItemName(entry.getItemName());
538                     itemsVo.setItemsManageTable(conf.getItemsManageTable());
539                     deleteItemsEntry(itemsVo);
540                 }
541                 itemNameToTableNameMap.remove(entry.getItemName());
542                 return true;
543             case ORPHAN_TABLE:
544             case VALID:
545             default:
546                 // Nothing to clean.
547                 return false;
548         }
549     }
550 }