]> git.basschouten.com Git - openhab-addons.git/blob
3ad12dbe61bf8bdc5dcd70c7874a0a2a6e7f4d42
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.mongodb.internal;
14
15 import java.time.ZoneId;
16 import java.time.ZonedDateTime;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.Date;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Map;
23 import java.util.Set;
24
25 import org.bson.Document;
26 import org.bson.types.ObjectId;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.core.items.Item;
30 import org.openhab.core.items.ItemNotFoundException;
31 import org.openhab.core.items.ItemRegistry;
32 import org.openhab.core.library.items.NumberItem;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.persistence.FilterCriteria;
35 import org.openhab.core.persistence.FilterCriteria.Ordering;
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.osgi.framework.BundleContext;
45 import org.osgi.service.component.annotations.Activate;
46 import org.osgi.service.component.annotations.Component;
47 import org.osgi.service.component.annotations.ConfigurationPolicy;
48 import org.osgi.service.component.annotations.Deactivate;
49 import org.osgi.service.component.annotations.Reference;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import com.mongodb.client.MongoClient;
54 import com.mongodb.client.MongoClients;
55 import com.mongodb.client.MongoCollection;
56 import com.mongodb.client.MongoCursor;
57 import com.mongodb.client.result.DeleteResult;
58
59 /**
60  * This is the implementation of the MongoDB {@link PersistenceService}.
61  *
62  * @author Thorsten Hoeger - Initial contribution
63  * @author Stephan Brunner - Query fixes, Cleanup
64  * @author RenĂ© Ulbricht - Fixes type handling, driver update and cleanup
65  */
66 @NonNullByDefault
67 @Component(service = { PersistenceService.class, QueryablePersistenceService.class,
68         ModifiablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
69 public class MongoDBPersistenceService implements ModifiablePersistenceService {
70
71     private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
72
73     private String url = "";
74     private String db = "";
75     private String collection = "";
76     private boolean collectionPerItem;
77
78     private boolean initialized = false;
79
80     protected final ItemRegistry itemRegistry;
81
82     private @Nullable MongoClient cl;
83
84     @Activate
85     public MongoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
86         this.itemRegistry = itemRegistry;
87     }
88
89     @Activate
90     public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
91         @Nullable
92         String configUrl = (String) config.get("url");
93         logger.debug("MongoDB URL {}", configUrl);
94         if (configUrl == null || configUrl.isBlank()) {
95             logger.warn("The MongoDB database URL is missing - please configure the mongodb:url parameter.");
96             return;
97         }
98         url = configUrl;
99
100         @Nullable
101         String configDb = (String) config.get("database");
102         logger.debug("MongoDB database {}", configDb);
103         if (configDb == null || configDb.isBlank()) {
104             logger.warn("The MongoDB database name is missing - please configure the mongodb:database parameter.");
105             return;
106         }
107         db = configDb;
108
109         @Nullable
110         String dbCollection = (String) config.get("collection");
111         logger.debug("MongoDB collection {}", dbCollection);
112         collection = dbCollection == null ? "" : dbCollection;
113         collectionPerItem = dbCollection == null || dbCollection.isBlank();
114
115         if (!tryConnectToDatabase()) {
116             logger.warn("Failed to connect to MongoDB server. Trying to reconnect later.");
117         }
118
119         initialized = true;
120     }
121
122     @Deactivate
123     public void deactivate(final int reason) {
124         logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
125         disconnectFromDatabase();
126     }
127
128     @Override
129     public String getId() {
130         return "mongodb";
131     }
132
133     @Override
134     public String getLabel(@Nullable Locale locale) {
135         return "MongoDB";
136     }
137
138     @Override
139     public Set<PersistenceItemInfo> getItemInfo() {
140         return Collections.emptySet();
141     }
142
143     /**
144      * Checks if we have a database connection.
145      * Also tests if communication with the MongoDB-Server is available.
146      *
147      * @return true if connection has been established, false otherwise
148      */
149     private synchronized boolean isConnected() {
150         MongoClient localCl = cl;
151         if (localCl == null) {
152             return false;
153         }
154
155         // Also check if the connection is valid.
156         // Network problems may cause failure sometimes,
157         // even if the connection object was successfully created before.
158         try {
159             localCl.listDatabaseNames().first();
160             return true;
161         } catch (Exception ex) {
162             return false;
163         }
164     }
165
166     /**
167      * (Re)connects to the database
168      *
169      * @return True, if the connection was successfully established.
170      */
171     private synchronized boolean tryConnectToDatabase() {
172         if (isConnected()) {
173             return true;
174         }
175
176         try {
177             logger.debug("Connect MongoDB");
178             disconnectFromDatabase();
179
180             this.cl = MongoClients.create(this.url);
181             MongoClient localCl = this.cl;
182
183             // The MongoDB driver always succeeds in creating the connection.
184             // We have to actually force it to test the connection to try to connect to the server.
185             if (localCl != null) {
186                 localCl.listDatabaseNames().first();
187                 logger.debug("Connect MongoDB ... done");
188                 return true;
189             }
190             return false;
191         } catch (Exception e) {
192             logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
193             disconnectFromDatabase();
194             return false;
195         }
196     }
197
198     /**
199      * Fetches the currently valid database.
200      *
201      * @return The database object
202      */
203     private synchronized @Nullable MongoClient getDatabase() {
204         return cl;
205     }
206
207     /**
208      * Connects to the Collection
209      *
210      * @return The collection object when collection creation was successful. Null otherwise.
211      */
212     private @Nullable MongoCollection<Document> connectToCollection(String collectionName) {
213         try {
214             @Nullable
215             MongoClient db = getDatabase();
216
217             if (db == null) {
218                 logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
219                 return null;
220             }
221
222             MongoCollection<Document> mongoCollection = db.getDatabase(this.db).getCollection(collectionName);
223
224             Document idx = new Document();
225             idx.append(MongoDBFields.FIELD_ITEM, 1).append(MongoDBFields.FIELD_TIMESTAMP, 1);
226             mongoCollection.createIndex(idx);
227
228             return mongoCollection;
229         } catch (Exception e) {
230             logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
231             return null;
232         }
233     }
234
235     /**
236      * Disconnects from the database
237      */
238     private synchronized void disconnectFromDatabase() {
239         MongoClient localCl = cl;
240         if (localCl != null) {
241             localCl.close();
242         }
243
244         cl = null;
245     }
246
247     @Override
248     public Iterable<HistoricItem> query(FilterCriteria filter) {
249         MongoCollection<Document> collection = prepareCollection(filter);
250         // If collection creation failed, return nothing.
251         if (collection == null) {
252             // Logging is done in connectToCollection()
253             return Collections.emptyList();
254         }
255
256         Document query = createQuery(filter);
257         if (query == null) {
258             return Collections.emptyList();
259         }
260
261         @Nullable
262         String realItemName = filter.getItemName();
263         if (realItemName == null) {
264             logger.warn("Item name is missing in filter {}", filter);
265             return Collections.emptyList();
266         }
267
268         Item item = getItem(realItemName);
269         if (item == null) {
270             logger.warn("Item {} not found", realItemName);
271             return Collections.emptyList();
272         }
273         List<HistoricItem> items = new ArrayList<>();
274
275         logger.debug("Query: {}", query);
276
277         Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
278         MongoCursor<Document> cursor = null;
279         try {
280             cursor = collection.find(query).sort(new Document(MongoDBFields.FIELD_TIMESTAMP, sortDir))
281                     .skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize()).iterator();
282
283             while (cursor.hasNext()) {
284                 Document obj = cursor.next();
285
286                 final State state = MongoDBTypeConversions.getStateFromDocument(item, obj);
287
288                 items.add(new MongoDBItem(realItemName, state, ZonedDateTime
289                         .ofInstant(obj.getDate(MongoDBFields.FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
290             }
291         } finally {
292             if (cursor != null) {
293                 cursor.close();
294             }
295         }
296
297         return items;
298     }
299
300     private @Nullable Item getItem(String itemName) {
301         try {
302             return itemRegistry.getItem(itemName);
303         } catch (ItemNotFoundException e1) {
304             logger.error("Unable to get item type for {}", itemName);
305         }
306         return null;
307     }
308
309     @Override
310     public List<PersistenceStrategy> getDefaultStrategies() {
311         return Collections.emptyList();
312     }
313
314     @Override
315     public void store(Item item, @Nullable String alias) {
316         store(item, new Date(), item.getState(), alias);
317     }
318
319     @Override
320     public void store(Item item) {
321         store(item, null);
322     }
323
324     @Override
325     public void store(Item item, ZonedDateTime date, State state) {
326         store(item, date, state, null);
327     }
328
329     @Override
330     public void store(Item item, ZonedDateTime date, State state, @Nullable String alias) {
331         Date dateConverted = Date.from(date.toInstant());
332         store(item, dateConverted, state, alias);
333     }
334
335     private void store(Item item, Date date, State state, @Nullable String alias) {
336         // Don't log undefined/uninitialized data
337         if (state instanceof UnDefType) {
338             return;
339         }
340
341         // If we've not initialized the bundle, then return
342         if (!initialized) {
343             logger.warn("MongoDB not initialized");
344             return;
345         }
346
347         // Connect to mongodb server if we're not already connected
348         // If we can't connect, log.
349         if (!tryConnectToDatabase()) {
350             logger.warn(
351                     "mongodb: No connection to database. Cannot persist item '{}'! Will retry connecting to database next time.",
352                     item);
353             return;
354         }
355
356         String realItemName = item.getName();
357         String collectionName = collectionPerItem ? realItemName : this.collection;
358
359         @Nullable
360         MongoCollection<Document> collection = connectToCollection(collectionName);
361
362         if (collection == null) {
363             // Logging is done in connectToCollection()
364             return;
365         }
366
367         String name = (alias != null) ? alias : realItemName;
368         Object value = MongoDBTypeConversions.convertValue(state);
369
370         Document obj = new Document();
371         obj.put(MongoDBFields.FIELD_ID, new ObjectId());
372         obj.put(MongoDBFields.FIELD_ITEM, name);
373         obj.put(MongoDBFields.FIELD_REALNAME, realItemName);
374         obj.put(MongoDBFields.FIELD_TIMESTAMP, date);
375         obj.put(MongoDBFields.FIELD_VALUE, value);
376         if (item instanceof NumberItem && state instanceof QuantityType<?>) {
377             obj.put(MongoDBFields.FIELD_UNIT, ((QuantityType<?>) state).getUnit().toString());
378         }
379         try {
380             collection.insertOne(obj);
381         } catch (org.bson.BsonMaximumSizeExceededException e) {
382             logger.error("Document size exceeds maximum size of 16MB. Item {} not persisted.", name);
383             throw e;
384         }
385         logger.debug("MongoDB save {}={}", name, value);
386     }
387
388     @Nullable
389     public MongoCollection<Document> prepareCollection(FilterCriteria filter) {
390         if (!initialized || !tryConnectToDatabase()) {
391             return null;
392         }
393
394         String realItemName = filter.getItemName();
395         if (realItemName == null) {
396             logger.warn("Item name is missing in filter {}", filter);
397             return null;
398         }
399
400         @Nullable
401         MongoCollection<Document> collection = getCollection(realItemName);
402         return collection;
403     }
404
405     @Nullable
406     private MongoCollection<Document> getCollection(String realItemName) {
407         String collectionName = collectionPerItem ? realItemName : this.collection;
408         @Nullable
409         MongoCollection<Document> collection = connectToCollection(collectionName);
410
411         if (collection == null) {
412             // Logging is done in connectToCollection()
413             logger.warn("Failed to connect to collection {}", collectionName);
414         }
415
416         return collection;
417     }
418
419     @Nullable
420     private Document createQuery(FilterCriteria filter) {
421         String realItemName = filter.getItemName();
422         Document query = new Document();
423         query.put(MongoDBFields.FIELD_ITEM, realItemName);
424
425         if (!addStateToQuery(filter, query) || !addDateToQuery(filter, query)) {
426             return null;
427         }
428
429         return query;
430     }
431
432     private boolean addStateToQuery(FilterCriteria filter, Document query) {
433         State filterState = filter.getState();
434         if (filterState != null) {
435             String op = MongoDBTypeConversions.convertOperator(filter.getOperator());
436
437             if (op == null) {
438                 logger.error("Failed to convert operator {} to MongoDB operator", filter.getOperator());
439                 return false;
440             }
441
442             Object value = MongoDBTypeConversions.convertValue(filterState);
443             query.put(MongoDBFields.FIELD_VALUE, new Document(op, value));
444         }
445
446         return true;
447     }
448
449     private boolean addDateToQuery(FilterCriteria filter, Document query) {
450         Document dateQueries = new Document();
451         ZonedDateTime beginDate = filter.getBeginDate();
452         if (beginDate != null) {
453             dateQueries.put("$gte", Date.from(beginDate.toInstant()));
454         }
455         ZonedDateTime endDate = filter.getEndDate();
456         if (endDate != null) {
457             dateQueries.put("$lte", Date.from(endDate.toInstant()));
458         }
459         if (!dateQueries.isEmpty()) {
460             query.put(MongoDBFields.FIELD_TIMESTAMP, dateQueries);
461         }
462
463         return true;
464     }
465
466     @Override
467     public boolean remove(FilterCriteria filter) {
468         MongoCollection<Document> collection = prepareCollection(filter);
469         // If collection creation failed, return nothing.
470         if (collection == null) {
471             // Logging is done in connectToCollection()
472             return false;
473         }
474
475         Document query = createQuery(filter);
476         if (query == null) {
477             return false;
478         }
479
480         logger.debug("Query: {}", query);
481
482         DeleteResult result = collection.deleteMany(query);
483
484         logger.debug("Deleted {} documents", result.getDeletedCount());
485         return true;
486     }
487 }