2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.mongodb.internal;
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;
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;
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;
60 * This is the implementation of the MongoDB {@link PersistenceService}.
62 * @author Thorsten Hoeger - Initial contribution
63 * @author Stephan Brunner - Query fixes, Cleanup
64 * @author René Ulbricht - Fixes type handling, driver update and cleanup
67 @Component(service = { PersistenceService.class, QueryablePersistenceService.class,
68 ModifiablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
69 public class MongoDBPersistenceService implements ModifiablePersistenceService {
71 private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
73 private String url = "";
74 private String db = "";
75 private String collection = "";
76 private boolean collectionPerItem;
78 private boolean initialized = false;
80 protected final ItemRegistry itemRegistry;
82 private @Nullable MongoClient cl;
85 public MongoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
86 this.itemRegistry = itemRegistry;
90 public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
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.");
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.");
110 String dbCollection = (String) config.get("collection");
111 logger.debug("MongoDB collection {}", dbCollection);
112 collection = dbCollection == null ? "" : dbCollection;
113 collectionPerItem = dbCollection == null || dbCollection.isBlank();
115 if (!tryConnectToDatabase()) {
116 logger.warn("Failed to connect to MongoDB server. Trying to reconnect later.");
123 public void deactivate(final int reason) {
124 logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
125 disconnectFromDatabase();
129 public String getId() {
134 public String getLabel(@Nullable Locale locale) {
139 public Set<PersistenceItemInfo> getItemInfo() {
140 return Collections.emptySet();
144 * Checks if we have a database connection.
145 * Also tests if communication with the MongoDB-Server is available.
147 * @return true if connection has been established, false otherwise
149 private synchronized boolean isConnected() {
150 MongoClient localCl = cl;
151 if (localCl == null) {
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.
159 localCl.listDatabaseNames().first();
161 } catch (Exception ex) {
167 * (Re)connects to the database
169 * @return True, if the connection was successfully established.
171 private synchronized boolean tryConnectToDatabase() {
177 logger.debug("Connect MongoDB");
178 disconnectFromDatabase();
180 this.cl = MongoClients.create(this.url);
181 MongoClient localCl = this.cl;
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");
191 } catch (Exception e) {
192 logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
193 disconnectFromDatabase();
199 * Fetches the currently valid database.
201 * @return The database object
203 private synchronized @Nullable MongoClient getDatabase() {
208 * Connects to the Collection
210 * @return The collection object when collection creation was successful. Null otherwise.
212 private @Nullable MongoCollection<Document> connectToCollection(String collectionName) {
215 MongoClient db = getDatabase();
218 logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
222 MongoCollection<Document> mongoCollection = db.getDatabase(this.db).getCollection(collectionName);
224 Document idx = new Document();
225 idx.append(MongoDBFields.FIELD_ITEM, 1).append(MongoDBFields.FIELD_TIMESTAMP, 1);
226 mongoCollection.createIndex(idx);
228 return mongoCollection;
229 } catch (Exception e) {
230 logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
236 * Disconnects from the database
238 private synchronized void disconnectFromDatabase() {
239 MongoClient localCl = cl;
240 if (localCl != null) {
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();
256 Document query = createQuery(filter);
258 return Collections.emptyList();
262 String realItemName = filter.getItemName();
263 if (realItemName == null) {
264 logger.warn("Item name is missing in filter {}", filter);
265 return Collections.emptyList();
268 Item item = getItem(realItemName);
270 logger.warn("Item {} not found", realItemName);
271 return Collections.emptyList();
273 List<HistoricItem> items = new ArrayList<>();
275 logger.debug("Query: {}", query);
277 Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
278 MongoCursor<Document> cursor = null;
280 cursor = collection.find(query).sort(new Document(MongoDBFields.FIELD_TIMESTAMP, sortDir))
281 .skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize()).iterator();
283 while (cursor.hasNext()) {
284 Document obj = cursor.next();
286 final State state = MongoDBTypeConversions.getStateFromDocument(item, obj);
288 items.add(new MongoDBItem(realItemName, state, ZonedDateTime
289 .ofInstant(obj.getDate(MongoDBFields.FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
292 if (cursor != null) {
300 private @Nullable Item getItem(String itemName) {
302 return itemRegistry.getItem(itemName);
303 } catch (ItemNotFoundException e1) {
304 logger.error("Unable to get item type for {}", itemName);
310 public List<PersistenceStrategy> getDefaultStrategies() {
311 return Collections.emptyList();
315 public void store(Item item, @Nullable String alias) {
316 store(item, new Date(), item.getState(), alias);
320 public void store(Item item) {
325 public void store(Item item, ZonedDateTime date, State state) {
326 store(item, date, state, null);
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);
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) {
341 // If we've not initialized the bundle, then return
343 logger.warn("MongoDB not initialized");
347 // Connect to mongodb server if we're not already connected
348 // If we can't connect, log.
349 if (!tryConnectToDatabase()) {
351 "mongodb: No connection to database. Cannot persist item '{}'! Will retry connecting to database next time.",
356 String realItemName = item.getName();
357 String collectionName = collectionPerItem ? realItemName : this.collection;
360 MongoCollection<Document> collection = connectToCollection(collectionName);
362 if (collection == null) {
363 // Logging is done in connectToCollection()
367 String name = (alias != null) ? alias : realItemName;
368 Object value = MongoDBTypeConversions.convertValue(state);
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());
380 collection.insertOne(obj);
381 } catch (org.bson.BsonMaximumSizeExceededException e) {
382 logger.error("Document size exceeds maximum size of 16MB. Item {} not persisted.", name);
385 logger.debug("MongoDB save {}={}", name, value);
389 public MongoCollection<Document> prepareCollection(FilterCriteria filter) {
390 if (!initialized || !tryConnectToDatabase()) {
394 String realItemName = filter.getItemName();
395 if (realItemName == null) {
396 logger.warn("Item name is missing in filter {}", filter);
401 MongoCollection<Document> collection = getCollection(realItemName);
406 private MongoCollection<Document> getCollection(String realItemName) {
407 String collectionName = collectionPerItem ? realItemName : this.collection;
409 MongoCollection<Document> collection = connectToCollection(collectionName);
411 if (collection == null) {
412 // Logging is done in connectToCollection()
413 logger.warn("Failed to connect to collection {}", collectionName);
420 private Document createQuery(FilterCriteria filter) {
421 String realItemName = filter.getItemName();
422 Document query = new Document();
423 query.put(MongoDBFields.FIELD_ITEM, realItemName);
425 if (!addStateToQuery(filter, query) || !addDateToQuery(filter, query)) {
432 private boolean addStateToQuery(FilterCriteria filter, Document query) {
433 State filterState = filter.getState();
434 if (filterState != null) {
435 String op = MongoDBTypeConversions.convertOperator(filter.getOperator());
438 logger.error("Failed to convert operator {} to MongoDB operator", filter.getOperator());
442 Object value = MongoDBTypeConversions.convertValue(filterState);
443 query.put(MongoDBFields.FIELD_VALUE, new Document(op, value));
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()));
455 ZonedDateTime endDate = filter.getEndDate();
456 if (endDate != null) {
457 dateQueries.put("$lte", Date.from(endDate.toInstant()));
459 if (!dateQueries.isEmpty()) {
460 query.put(MongoDBFields.FIELD_TIMESTAMP, dateQueries);
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()
475 Document query = createQuery(filter);
480 logger.debug("Query: {}", query);
482 DeleteResult result = collection.deleteMany(query);
484 logger.debug("Deleted {} documents", result.getDeletedCount());