[mongodb] Upgrade DB driver, add more type handlings, fix QuantityType handling (#16333)

* #16308 #16310 Upgraded MongoDB driver, added initial unit tests
* #16308 #16310 Refactored the MongoDBPersistence adding helper, fixing type handling for HSBType, RawType and QuantityType
* #16308 Added backwardcompatibility for the old way of writting the data where possible
* #16308 Added test for larger ImageItems and the limit of 16 MB

Signed-off-by: René Ulbricht <rene_ulbricht@outlook.com>
This commit is contained in:
ulbi 2024-02-17 10:58:14 +01:00 committed by GitHub
parent 2db9fb027d
commit 956b8e47d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 2491 additions and 260 deletions

View File

@ -14,12 +14,66 @@
<name>openHAB Add-ons :: Bundles :: Persistence Service :: MongoDB</name>
<properties>
<bnd.importpackage>!sun.nio.ch;!org.bson.codecs.kotlin*;!jnr.unixsocket*;!javax.annotation*;!com.google*;!io.netty*;com.oracle*;resolution:=optional;com.aayushatharva*;resolution:=optional;com.mongodb.crypt*;resolution:=optional;com.amazon*;resolution:=optional;software.amazon*;resolution:=optional</bnd.importpackage>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.mongodb/mongo-java-driver -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.13.1</version>
<artifactId>mongodb-driver-sync</artifactId>
<version>4.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
<version>4.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId>
<version>4.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
<version>1.1.10.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.5-3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson-record-codec</artifactId>
<version>4.11.1</version>
<scope>compile</scope>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>de.bwaldvogel</groupId>
<artifactId>mongo-java-server</artifactId>
<version>1.44.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class defines constant field names used in MongoDB documents.
* These field names are used to ensure consistent access to document properties.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public final class MongoDBFields {
public static final String FIELD_ID = "_id";
public static final String FIELD_ITEM = "item";
public static final String FIELD_REALNAME = "realName";
public static final String FIELD_TIMESTAMP = "timestamp";
public static final String FIELD_VALUE = "value";
public static final String FIELD_UNIT = "unit";
public static final String FIELD_VALUE_DATA = "value.data";
public static final String FIELD_VALUE_TYPE = "value.type";
private MongoDBFields() {
// Private constructor to prevent instantiation
}
}

View File

@ -14,6 +14,7 @@ package org.openhab.persistence.mongodb.internal;
import java.text.DateFormat;
import java.time.ZonedDateTime;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.persistence.HistoricItem;
@ -54,6 +55,7 @@ public class MongoDBItem implements HistoricItem {
@Override
public String toString() {
return DateFormat.getDateTimeInstance().format(timestamp) + ": " + name + " -> " + state.toString();
Date date = Date.from(timestamp.toInstant());
return DateFormat.getDateTimeInstance().format(date) + ": " + name + " -> " + state.toString();
}
}

View File

@ -22,28 +22,19 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.FilterCriteria.Operator;
import org.openhab.core.persistence.FilterCriteria.Ordering;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.ModifiablePersistenceService;
import org.openhab.core.persistence.PersistenceItemInfo;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
@ -59,29 +50,23 @@ import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.result.DeleteResult;
/**
* This is the implementation of the MongoDB {@link PersistenceService}.
*
* @author Thorsten Hoeger - Initial contribution
* @author Stephan Brunner - Query fixes, Cleanup
* @author René Ulbricht - Fixes type handling, driver update and cleanup
*/
@NonNullByDefault
@Component(service = { PersistenceService.class,
QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
public class MongoDBPersistenceService implements QueryablePersistenceService {
private static final String FIELD_ID = "_id";
private static final String FIELD_ITEM = "item";
private static final String FIELD_REALNAME = "realName";
private static final String FIELD_TIMESTAMP = "timestamp";
private static final String FIELD_VALUE = "value";
@Component(service = { PersistenceService.class, QueryablePersistenceService.class,
ModifiablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
public class MongoDBPersistenceService implements ModifiablePersistenceService {
private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
@ -150,10 +135,206 @@ public class MongoDBPersistenceService implements QueryablePersistenceService {
return "MongoDB";
}
@Override
public Set<PersistenceItemInfo> getItemInfo() {
return Collections.emptySet();
}
/**
* Checks if we have a database connection.
* Also tests if communication with the MongoDB-Server is available.
*
* @return true if connection has been established, false otherwise
*/
private synchronized boolean isConnected() {
MongoClient localCl = cl;
if (localCl == null) {
return false;
}
// Also check if the connection is valid.
// Network problems may cause failure sometimes,
// even if the connection object was successfully created before.
try {
localCl.listDatabaseNames().first();
return true;
} catch (Exception ex) {
return false;
}
}
/**
* (Re)connects to the database
*
* @return True, if the connection was successfully established.
*/
private synchronized boolean tryConnectToDatabase() {
if (isConnected()) {
return true;
}
try {
logger.debug("Connect MongoDB");
disconnectFromDatabase();
this.cl = MongoClients.create(this.url);
MongoClient localCl = this.cl;
// The MongoDB driver always succeeds in creating the connection.
// We have to actually force it to test the connection to try to connect to the server.
if (localCl != null) {
localCl.listDatabaseNames().first();
logger.debug("Connect MongoDB ... done");
return true;
}
return false;
} catch (Exception e) {
logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
disconnectFromDatabase();
return false;
}
}
/**
* Fetches the currently valid database.
*
* @return The database object
*/
private synchronized @Nullable MongoClient getDatabase() {
return cl;
}
/**
* Connects to the Collection
*
* @return The collection object when collection creation was successful. Null otherwise.
*/
private @Nullable MongoCollection<Document> connectToCollection(String collectionName) {
try {
@Nullable
MongoClient db = getDatabase();
if (db == null) {
logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
return null;
}
MongoCollection<Document> mongoCollection = db.getDatabase(this.db).getCollection(collectionName);
Document idx = new Document();
idx.append(MongoDBFields.FIELD_ITEM, 1).append(MongoDBFields.FIELD_TIMESTAMP, 1);
mongoCollection.createIndex(idx);
return mongoCollection;
} catch (Exception e) {
logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
return null;
}
}
/**
* Disconnects from the database
*/
private synchronized void disconnectFromDatabase() {
MongoClient localCl = cl;
if (localCl != null) {
localCl.close();
}
cl = null;
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
MongoCollection<Document> collection = prepareCollection(filter);
// If collection creation failed, return nothing.
if (collection == null) {
// Logging is done in connectToCollection()
return Collections.emptyList();
}
Document query = createQuery(filter);
if (query == null) {
return Collections.emptyList();
}
@Nullable
String realItemName = filter.getItemName();
if (realItemName == null) {
logger.warn("Item name is missing in filter {}", filter);
return Collections.emptyList();
}
Item item = getItem(realItemName);
if (item == null) {
logger.warn("Item {} not found", realItemName);
return Collections.emptyList();
}
List<HistoricItem> items = new ArrayList<>();
logger.debug("Query: {}", query);
Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
MongoCursor<Document> cursor = null;
try {
cursor = collection.find(query).sort(new Document(MongoDBFields.FIELD_TIMESTAMP, sortDir))
.skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize()).iterator();
while (cursor.hasNext()) {
Document obj = cursor.next();
final State state = MongoDBTypeConversions.getStateFromDocument(item, obj);
items.add(new MongoDBItem(realItemName, state, ZonedDateTime
.ofInstant(obj.getDate(MongoDBFields.FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
}
private @Nullable Item getItem(String itemName) {
try {
return itemRegistry.getItem(itemName);
} catch (ItemNotFoundException e1) {
logger.error("Unable to get item type for {}", itemName);
}
return null;
}
@Override
public List<PersistenceStrategy> getDefaultStrategies() {
return Collections.emptyList();
}
@Override
public void store(Item item, @Nullable String alias) {
store(item, new Date(), item.getState(), alias);
}
@Override
public void store(Item item) {
store(item, null);
}
@Override
public void store(Item item, ZonedDateTime date, State state) {
store(item, date, state, null);
}
@Override
public void store(Item item, ZonedDateTime date, State state, @Nullable String alias) {
Date dateConverted = Date.from(date.toInstant());
store(item, dateConverted, state, alias);
}
private void store(Item item, Date date, State state, @Nullable String alias) {
// Don't log undefined/uninitialized data
if (item.getState() instanceof UnDefType) {
if (state instanceof UnDefType) {
return;
}
@ -176,7 +357,7 @@ public class MongoDBPersistenceService implements QueryablePersistenceService {
String collectionName = collectionPerItem ? realItemName : this.collection;
@Nullable
DBCollection collection = connectToCollection(collectionName);
MongoCollection<Document> collection = connectToCollection(collectionName);
if (collection == null) {
// Logging is done in connectToCollection()
@ -184,270 +365,123 @@ public class MongoDBPersistenceService implements QueryablePersistenceService {
}
String name = (alias != null) ? alias : realItemName;
Object value = this.convertValue(item.getState());
DBObject obj = new BasicDBObject();
obj.put(FIELD_ID, new ObjectId());
obj.put(FIELD_ITEM, name);
obj.put(FIELD_REALNAME, realItemName);
obj.put(FIELD_TIMESTAMP, new Date());
obj.put(FIELD_VALUE, value);
collection.save(obj);
Object value = MongoDBTypeConversions.convertValue(state);
Document obj = new Document();
obj.put(MongoDBFields.FIELD_ID, new ObjectId());
obj.put(MongoDBFields.FIELD_ITEM, name);
obj.put(MongoDBFields.FIELD_REALNAME, realItemName);
obj.put(MongoDBFields.FIELD_TIMESTAMP, date);
obj.put(MongoDBFields.FIELD_VALUE, value);
if (item instanceof NumberItem && state instanceof QuantityType<?>) {
obj.put(MongoDBFields.FIELD_UNIT, ((QuantityType<?>) state).getUnit().toString());
}
try {
collection.insertOne(obj);
} catch (org.bson.BsonMaximumSizeExceededException e) {
logger.error("Document size exceeds maximum size of 16MB. Item {} not persisted.", name);
throw e;
}
logger.debug("MongoDB save {}={}", name, value);
}
private Object convertValue(State state) {
Object value;
if (state instanceof PercentType type) {
value = type.toBigDecimal().doubleValue();
} else if (state instanceof DateTimeType type) {
value = Date.from(type.getZonedDateTime().toInstant());
} else if (state instanceof DecimalType type) {
value = type.toBigDecimal().doubleValue();
} else {
value = state.toString();
}
return value;
}
@Override
public void store(Item item) {
store(item, null);
}
@Override
public Set<PersistenceItemInfo> getItemInfo() {
return Collections.emptySet();
}
/**
* Checks if we have a database connection.
* Also tests if communication with the MongoDB-Server is available.
*
* @return true if connection has been established, false otherwise
*/
private synchronized boolean isConnected() {
if (cl == null) {
return false;
}
// Also check if the connection is valid.
// Network problems may cause failure sometimes,
// even if the connection object was successfully created before.
try {
cl.getAddress();
return true;
} catch (Exception ex) {
return false;
}
}
/**
* (Re)connects to the database
*
* @return True, if the connection was successfully established.
*/
private synchronized boolean tryConnectToDatabase() {
if (isConnected()) {
return true;
}
try {
logger.debug("Connect MongoDB");
disconnectFromDatabase();
this.cl = new MongoClient(new MongoClientURI(this.url));
// The mongo always succeeds in creating the connection.
// We have to actually force it to test the connection to try to connect to the server.
cl.getAddress();
logger.debug("Connect MongoDB ... done");
return true;
} catch (Exception e) {
logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
disconnectFromDatabase();
return false;
}
}
/**
* Fetches the currently valid database.
*
* @return The database object
*/
private synchronized @Nullable MongoClient getDatabase() {
return cl;
}
/**
* Connects to the Collection
*
* @return The collection object when collection creation was successful. Null otherwise.
*/
private @Nullable DBCollection connectToCollection(String collectionName) {
try {
@Nullable
MongoClient db = getDatabase();
if (db == null) {
logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
return null;
}
DBCollection mongoCollection = db.getDB(this.db).getCollection(collectionName);
BasicDBObject idx = new BasicDBObject();
idx.append(FIELD_ITEM, 1).append(FIELD_TIMESTAMP, 1);
mongoCollection.createIndex(idx);
return mongoCollection;
} catch (Exception e) {
logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
@Nullable
public MongoCollection<Document> prepareCollection(FilterCriteria filter) {
if (!initialized || !tryConnectToDatabase()) {
return null;
}
}
/**
* Disconnects from the database
*/
private synchronized void disconnectFromDatabase() {
if (this.cl != null) {
this.cl.close();
}
cl = null;
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
if (!initialized) {
return Collections.emptyList();
}
if (!tryConnectToDatabase()) {
return Collections.emptyList();
}
String realItemName = filter.getItemName();
if (realItemName == null) {
logger.warn("Item name is missing in filter {}", filter);
return List.of();
return null;
}
@Nullable
MongoCollection<Document> collection = getCollection(realItemName);
return collection;
}
@Nullable
private MongoCollection<Document> getCollection(String realItemName) {
String collectionName = collectionPerItem ? realItemName : this.collection;
@Nullable
DBCollection collection = connectToCollection(collectionName);
MongoCollection<Document> collection = connectToCollection(collectionName);
// If collection creation failed, return nothing.
if (collection == null) {
// Logging is done in connectToCollection()
return Collections.emptyList();
logger.warn("Failed to connect to collection {}", collectionName);
}
@Nullable
Item item = getItem(realItemName);
return collection;
}
if (item == null) {
logger.warn("Item {} not found", realItemName);
return Collections.emptyList();
@Nullable
private Document createQuery(FilterCriteria filter) {
String realItemName = filter.getItemName();
Document query = new Document();
query.put(MongoDBFields.FIELD_ITEM, realItemName);
if (!addStateToQuery(filter, query) || !addDateToQuery(filter, query)) {
return null;
}
List<HistoricItem> items = new ArrayList<>();
BasicDBObject query = new BasicDBObject();
if (filter.getItemName() != null) {
query.put(FIELD_ITEM, filter.getItemName());
}
return query;
}
private boolean addStateToQuery(FilterCriteria filter, Document query) {
State filterState = filter.getState();
if (filterState != null && filter.getOperator() != null) {
@Nullable
String op = convertOperator(filter.getOperator());
if (filterState != null) {
String op = MongoDBTypeConversions.convertOperator(filter.getOperator());
if (op == null) {
logger.error("Failed to convert operator {} to MongoDB operator", filter.getOperator());
return Collections.emptyList();
return false;
}
Object value = convertValue(filterState);
query.put(FIELD_VALUE, new BasicDBObject(op, value));
Object value = MongoDBTypeConversions.convertValue(filterState);
query.put(MongoDBFields.FIELD_VALUE, new Document(op, value));
}
BasicDBObject dateQueries = new BasicDBObject();
if (filter.getBeginDate() != null) {
dateQueries.put("$gte", Date.from(filter.getBeginDate().toInstant()));
return true;
}
private boolean addDateToQuery(FilterCriteria filter, Document query) {
Document dateQueries = new Document();
ZonedDateTime beginDate = filter.getBeginDate();
if (beginDate != null) {
dateQueries.put("$gte", Date.from(beginDate.toInstant()));
}
if (filter.getEndDate() != null) {
dateQueries.put("$lte", Date.from(filter.getEndDate().toInstant()));
ZonedDateTime endDate = filter.getEndDate();
if (endDate != null) {
dateQueries.put("$lte", Date.from(endDate.toInstant()));
}
if (!dateQueries.isEmpty()) {
query.put(FIELD_TIMESTAMP, dateQueries);
query.put(MongoDBFields.FIELD_TIMESTAMP, dateQueries);
}
return true;
}
@Override
public boolean remove(FilterCriteria filter) {
MongoCollection<Document> collection = prepareCollection(filter);
// If collection creation failed, return nothing.
if (collection == null) {
// Logging is done in connectToCollection()
return false;
}
Document query = createQuery(filter);
if (query == null) {
return false;
}
logger.debug("Query: {}", query);
Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
DBCursor cursor = collection.find(query).sort(new BasicDBObject(FIELD_TIMESTAMP, sortDir))
.skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize());
DeleteResult result = collection.deleteMany(query);
while (cursor.hasNext()) {
BasicDBObject obj = (BasicDBObject) cursor.next();
final State state;
if (item instanceof NumberItem) {
state = new DecimalType(obj.getDouble(FIELD_VALUE));
} else if (item instanceof DimmerItem) {
state = new PercentType(obj.getInt(FIELD_VALUE));
} else if (item instanceof SwitchItem) {
state = OnOffType.valueOf(obj.getString(FIELD_VALUE));
} else if (item instanceof ContactItem) {
state = OpenClosedType.valueOf(obj.getString(FIELD_VALUE));
} else if (item instanceof RollershutterItem) {
state = new PercentType(obj.getInt(FIELD_VALUE));
} else if (item instanceof DateTimeItem) {
state = new DateTimeType(
ZonedDateTime.ofInstant(obj.getDate(FIELD_VALUE).toInstant(), ZoneId.systemDefault()));
} else {
state = new StringType(obj.getString(FIELD_VALUE));
}
items.add(new MongoDBItem(realItemName, state,
ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
}
return items;
}
private @Nullable String convertOperator(Operator operator) {
switch (operator) {
case EQ:
return "$eq";
case GT:
return "$gt";
case GTE:
return "$gte";
case LT:
return "$lt";
case LTE:
return "$lte";
case NEQ:
return "$neq";
default:
return null;
}
}
private @Nullable Item getItem(String itemName) {
try {
return itemRegistry.getItem(itemName);
} catch (ItemNotFoundException e1) {
logger.error("Unable to get item type for {}", itemName);
}
return null;
}
@Override
public List<PersistenceStrategy> getDefaultStrategies() {
return Collections.emptyList();
logger.debug("Deleted {} documents", result.getDeletedCount());
return true;
}
}

View File

@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import javax.measure.Unit;
import org.bson.Document;
import org.bson.types.Binary;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.CallItem;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.ImageItem;
import org.openhab.core.library.items.LocationItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.PlayerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.persistence.FilterCriteria.Operator;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class handles the conversion of types between openHAB and MongoDB.
* It provides methods to convert openHAB states to MongoDB compatible types and vice versa.
* It also provides a method to convert openHAB filter operators to MongoDB query operators.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public class MongoDBTypeConversions {
/**
* Converts a MongoDB document to an openHAB state.
*
* @param item The openHAB item that the state belongs to.
* @param doc The MongoDB document to convert.
* @return The openHAB state.
* @throws IllegalArgumentException If the item type is not supported.
*/
public static State getStateFromDocument(Item item, Document doc) {
BiFunction<Item, Document, State> converter = ITEM_STATE_CONVERTERS.get(item.getClass());
if (converter != null) {
return converter.apply(item, doc);
} else {
throw new IllegalArgumentException("Unsupported item type: " + item.getClass().getName());
}
}
/**
* Converts an openHAB filter operator to a MongoDB query operator.
*
* @param operator The openHAB filter operator to convert.
* @return The MongoDB query operator, or null if the operator is not supported.
*/
public static @Nullable String convertOperator(Operator operator) {
return switch (operator) {
case EQ -> "$eq";
case GT -> "$gt";
case GTE -> "$gte";
case LT -> "$lt";
case LTE -> "$lte";
case NEQ -> "$neq";
default -> null;
};
}
/**
* Converts an openHAB state to a MongoDB compatible type.
*
* @param state The openHAB state to convert.
* @return The MongoDB compatible type.
*/
public static Object convertValue(State state) {
return STATE_CONVERTERS.getOrDefault(state.getClass(), State::toString).apply(state);
}
private static final Logger logger = LoggerFactory.getLogger(MongoDBTypeConversions.class);
/**
* A map of converters that convert openHAB states to MongoDB compatible types.
* Each converter is a function that takes an openHAB state and returns an object that can be stored in MongoDB.
*/
private static final Map<Class<? extends State>, Function<State, Object>> STATE_CONVERTERS = Map.of( //
HSBType.class, State::toString, //
QuantityType.class, state -> ((QuantityType<?>) state).toBigDecimal().doubleValue(), //
PercentType.class, state -> ((PercentType) state).intValue(), //
DateTimeType.class, state -> ((DateTimeType) state).getZonedDateTime().toString(), //
StringListType.class, State::toString, //
DecimalType.class, state -> ((DecimalType) state).toBigDecimal().doubleValue(), //
RawType.class, MongoDBTypeConversions::handleRawType//
);
private static Object handleRawType(State state) {
RawType rawType = (RawType) state;
Document doc = new Document();
doc.put(MongoDBFields.FIELD_VALUE_TYPE, rawType.getMimeType());
doc.put(MongoDBFields.FIELD_VALUE_DATA, rawType.getBytes());
return doc;
}
/**
* A map of converters that convert MongoDB documents to openHAB states.
* Each converter is a function that takes an openHAB item and a MongoDB document and returns an openHAB state.
*/
private static final Map<Class<? extends Item>, BiFunction<Item, Document, State>> ITEM_STATE_CONVERTERS = //
Map.ofEntries( //
Map.entry(NumberItem.class, MongoDBTypeConversions::handleNumberItem),
Map.entry(ColorItem.class, MongoDBTypeConversions::handleColorItem),
Map.entry(DimmerItem.class, MongoDBTypeConversions::handleDimmerItem),
Map.entry(SwitchItem.class,
(Item item, Document doc) -> OnOffType.valueOf(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(ContactItem.class,
(Item item, Document doc) -> OpenClosedType
.valueOf(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(RollershutterItem.class, MongoDBTypeConversions::handleRollershutterItem),
Map.entry(DateTimeItem.class, MongoDBTypeConversions::handleDateTimeItem),
Map.entry(LocationItem.class,
(Item item, Document doc) -> new PointType(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(PlayerItem.class,
(Item item, Document doc) -> PlayPauseType
.valueOf(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(CallItem.class,
(Item item, Document doc) -> new StringListType(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(ImageItem.class, MongoDBTypeConversions::handleImageItem), //
Map.entry(StringItem.class,
(Item item, Document doc) -> new StringType(doc.getString(MongoDBFields.FIELD_VALUE))),
Map.entry(GenericItem.class,
(Item item, Document doc) -> new StringType(doc.getString(MongoDBFields.FIELD_VALUE)))//
);
private static State handleNumberItem(Item item, Document doc) {
NumberItem numberItem = (NumberItem) item;
Unit<?> unit = numberItem.getUnit();
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value == null) {
return UnDefType.UNDEF;
}
if (doc.containsKey(MongoDBFields.FIELD_UNIT)) {
String unitString = doc.getString(MongoDBFields.FIELD_UNIT);
Unit<?> docUnit = UnitUtils.parseUnit(unitString);
if (docUnit != null) {
unit = docUnit;
}
}
if (value instanceof String) {
return new QuantityType<>(value.toString());
}
if (unit != null) {
return new QuantityType<>(((Number) value).doubleValue(), unit);
} else {
return new DecimalType(((Number) value).doubleValue());
}
}
private static State handleColorItem(Item item, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value instanceof String) {
return new HSBType(value.toString());
} else {
logger.warn("HSBType ({}) value is not a valid string: {}", doc.getString(MongoDBFields.FIELD_REALNAME),
value);
return new HSBType("0,0,0");
}
}
private static State handleDimmerItem(Item item, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value == null) {
return UnDefType.UNDEF;
}
if (value instanceof Integer) {
return new PercentType((Integer) value);
} else {
return new PercentType(((Number) value).intValue());
}
}
private static State handleRollershutterItem(Item item, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value == null) {
return UnDefType.UNDEF;
}
if (value instanceof Integer) {
return new PercentType((Integer) value);
} else {
return new PercentType(((Number) value).intValue());
}
}
private static State handleDateTimeItem(Item item, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value == null) {
return UnDefType.UNDEF;
}
if (value instanceof String) {
return new DateTimeType(ZonedDateTime.parse(doc.getString(MongoDBFields.FIELD_VALUE)));
} else {
return new DateTimeType(ZonedDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault()));
}
}
private static State handleImageItem(Item item, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
if (value instanceof Document) {
Document fieldValue = (Document) value;
String type = fieldValue.getString(MongoDBFields.FIELD_VALUE_TYPE);
Binary data = fieldValue.get(MongoDBFields.FIELD_VALUE_DATA, Binary.class);
return new RawType(data.getData(), type);
} else {
logger.warn("ImageItem ({}) value is not a Document: {}", doc.getString(MongoDBFields.FIELD_REALNAME),
value);
return new RawType(new byte[0], "application/octet-stream");
}
}
}

View File

@ -0,0 +1,444 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.Mockito;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.i18n.I18nProviderImpl;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.CallItem;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.ImageItem;
import org.openhab.core.library.items.LocationItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.PlayerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
/**
* This class provides helper methods to create test items.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public class DataCreationHelper {
protected static final UnitProvider UNIT_PROVIDER;
static {
ComponentContext context = Mockito.mock(ComponentContext.class);
BundleContext bundleContext = Mockito.mock(BundleContext.class);
Hashtable<String, Object> properties = new Hashtable<>();
properties.put("measurementSystem", SIUnits.MEASUREMENT_SYSTEM_NAME);
when(context.getProperties()).thenReturn(properties);
when(context.getBundleContext()).thenReturn(bundleContext);
UNIT_PROVIDER = new I18nProviderImpl(context);
}
/**
* Creates a NumberItem with a given name and value.
*
* @param name The name of the NumberItem.
* @param value The value of the NumberItem.
* @return The created NumberItem.
*/
public static NumberItem createNumberItem(String name, Number value) {
return createItem(NumberItem.class, name, new DecimalType(value));
}
/**
* Creates a StringItem with a given name and value.
*
* @param name The name of the StringItem.
* @param value The value of the StringItem.
* @return The created StringItem.
*/
public static StringItem createStringItem(String name, String value) {
return createItem(StringItem.class, name, new StringType(value));
}
/**
* Creates an instance of a NumberItem with a unit type and sets its state.
*
* @param itemType The Class object representing the type of the item to create.
* @param unitType The string representation of the unit type to set on the new item.
* @param name The name to give to the new item.
* @param state The state to set on the new item.
* @return The newly created item.
* @throws RuntimeException if an error occurs while creating the item or setting its state.
*/
public static NumberItem createNumberItem(String unitType, String name, State state) {
NumberItem item = new NumberItem(unitType, name, UNIT_PROVIDER);
item.setState(state);
return item;
}
/**
* Creates an instance of a specific GenericItem subclass and sets its state.
*
* @param <T> The type of the item to create. This must be a subclass of GenericItem.
* @param <S> The type of the state to set. This must be a subclass of State.
* @param itemType The Class object representing the type of the item to create.
* @param name The name to give to the new item.
* @param state The state to set on the new item.
* @return The newly created item.
* @throws RuntimeException if an error occurs while creating the item or setting its state.
*/
public static <T extends GenericItem, S extends State> T createItem(Class<T> itemType, String name, S state) {
try {
if (state == null) {
throw new IllegalArgumentException("State must not be null");
}
T item = itemType.getDeclaredConstructor(String.class).newInstance(name);
if (item == null) {
throw new RuntimeException("Could not create item");
}
item.setState(state);
return item;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static RawType createFakeImage(int size) {
byte[] data = new byte[size];
for (int i = 0; i < size; i++) {
data[i] = (byte) (i % 256);
}
return new RawType(data, "image/png");
}
/**
* Provides a stream of arguments for parameterized tests. To test various image sizes
*
* @return A stream of arguments for parameterized tests.
*/
public static Stream<Arguments> provideOpenhabImageItemsInDifferentSizes() {
return Stream.of(
Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem1kB", createFakeImage(1024))),
Arguments.of(
DataCreationHelper.createItem(ImageItem.class, "ImageItem1MB", createFakeImage(1024 * 1024))),
Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem10MB",
createFakeImage(10 * 1024 * 1024))),
Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem20MB",
createFakeImage(20 * 1024 * 1024))));
}
/**
* Provides a stream of arguments for parameterized tests. Each argument is an instance of a specific
* GenericItem subclass with a set state.
*
* @return A stream of arguments for parameterized tests.
*/
public static Stream<Arguments> provideOpenhabItemTypes() {
return Stream.of(
Arguments.of(
DataCreationHelper.createItem(StringItem.class, "StringItem", new StringType("StringValue"))),
Arguments.of(DataCreationHelper.createItem(NumberItem.class, "NumberItem", new DecimalType(123.45))),
Arguments.of(DataCreationHelper.createItem(DimmerItem.class, "DimmerItem", new PercentType(50))),
Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemON", OnOffType.ON)),
Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemOFF", OnOffType.OFF)),
Arguments.of(DataCreationHelper.createItem(ContactItem.class, "ContactItemOPEN", OpenClosedType.OPEN)),
Arguments.of(
DataCreationHelper.createItem(ContactItem.class, "ContactItemCLOSED", OpenClosedType.CLOSED)),
Arguments.of(DataCreationHelper.createItem(RollershutterItem.class, "RollershutterItem",
new PercentType(30))),
Arguments.of(DataCreationHelper.createItem(DateTimeItem.class, "DateTimeItem",
new DateTimeType(ZonedDateTime.now()))),
Arguments.of(DataCreationHelper.createItem(ColorItem.class, "ColorItem", new HSBType("180,100,100"))),
Arguments.of(
DataCreationHelper.createItem(LocationItem.class, "LocationItem", new PointType("51.0,0.0"))),
Arguments.of(DataCreationHelper.createItem(PlayerItem.class, "PlayerItem", PlayPauseType.PLAY)),
Arguments.of(DataCreationHelper.createItem(CallItem.class, "CallItem",
new StringListType("+49 123 456 789"))),
Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem",
new RawType(new byte[] { 0x00, 0x01, 0x02 }, "image/png"))),
Arguments.of(DataCreationHelper.createNumberItem("Number:Energy", "NumberItemCelcius",
new QuantityType<>("25.00 MWh"))),
Arguments.of(DataCreationHelper.createNumberItem("Number:Temperature", "NumberItemCelcius",
new QuantityType<>("25.00 °F"))));
}
/**
* Provides a stream of arguments to be used for parameterized tests.
*
* Each argument is a DatabaseTestContainer instance. Some instances use a MemoryBackend,
* while others use a MongoDBContainer with a specific MongoDB version.
* In case there is no Docker available, only the MemoryBackend is used.
*
* @return A stream of Arguments, each containing a DatabaseTestContainer instance.
*/
public static Stream<Arguments> provideDatabaseBackends() {
if (DockerClientFactory.instance().isDockerAvailable()) {
// If Docker is available, create a stream of Arguments with all backends
return Stream.of(
// Create a DatabaseTestContainer with a MemoryBackend
Arguments.of(new DatabaseTestContainer(new MemoryBackend())),
// Create DatabaseTestContainers with MongoDBContainers of specific versions
Arguments.of(new DatabaseTestContainer("mongo:3.6")),
Arguments.of(new DatabaseTestContainer("mongo:4.4")),
Arguments.of(new DatabaseTestContainer("mongo:5.0")),
Arguments.of(new DatabaseTestContainer("mongo:6.0")));
} else {
// If Docker is not available, create a stream of Arguments with only the MemoryBackend
return Stream.of(Arguments.of(new DatabaseTestContainer(new MemoryBackend())));
}
}
/**
* Creates a Document for a given item name, value, and timestamp.
*
* @param itemName The name of the item.
* @param value The value of the item.
* @param timestamp The timestamp of the item.
* @return The created Document.
*/
public static Document createDocument(String itemName, double value, LocalDate timestamp) {
Document obj = new Document();
obj.put(MongoDBFields.FIELD_ID, new ObjectId());
obj.put(MongoDBFields.FIELD_ITEM, itemName);
obj.put(MongoDBFields.FIELD_REALNAME, itemName);
obj.put(MongoDBFields.FIELD_TIMESTAMP, timestamp);
obj.put(MongoDBFields.FIELD_VALUE, value);
return obj;
}
/**
* Creates a FilterCriteria for a given item name.
*
* @param itemName The name of the item.
* @return The created FilterCriteria.
*/
public static FilterCriteria createFilterCriteria(String itemName) {
return createFilterCriteria(itemName, null, null);
}
/**
* Creates a FilterCriteria for a given item name, begin date, and end date.
*
* @param itemName The name of the item.
* @param beginDate The begin date of the FilterCriteria.
* @param endDate The end date of the FilterCriteria.
* @return The created FilterCriteria.
*/
public static FilterCriteria createFilterCriteria(String itemName, @Nullable ZonedDateTime beginDate,
@Nullable ZonedDateTime endDate) {
FilterCriteria filter = new FilterCriteria();
filter.setItemName(itemName);
filter.setPageSize(10);
filter.setPageNumber(0);
filter.setOrdering(FilterCriteria.Ordering.ASCENDING);
if (beginDate != null) {
filter.setBeginDate(beginDate);
}
if (endDate != null) {
filter.setEndDate(endDate);
}
return filter;
}
/**
* Sets up a MongoDB instance for testing.
*
* @param collectionName The name of the MongoDB collection to be used for testing.
* @param dbContainer The container running the MongoDB instance.
* @return A SetupResult object containing the MongoDBPersistenceService, the database, the bundle context, the
* configuration map, the item registry, and the database name.
*/
public static SetupResult setupMongoDB(@Nullable String collectionName, DatabaseTestContainer dbContainer) {
// Start the database container
dbContainer.start();
// Mock the ItemRegistry and BundleContext
ItemRegistry itemRegistry = Mockito.mock(ItemRegistry.class);
BundleContext bundleContext = Mockito.mock(BundleContext.class);
// When getService is called on the bundleContext, return the mocked itemRegistry
when(bundleContext.getService(any())).thenReturn(itemRegistry);
// Create a new MongoDBPersistenceService instance
MongoDBPersistenceService service = new MongoDBPersistenceService(itemRegistry);
// Create a configuration map for the MongoDBPersistenceService
Map<String, Object> config = new HashMap<>();
config.put("url", dbContainer.getConnectionString());
String dbname = UUID.randomUUID().toString();
config.put("database", dbname);
if (collectionName != null) {
config.put("collection", collectionName);
}
// Create a MongoClient connected to the mock server
MongoClient mongoClient = MongoClients.create(dbContainer.getConnectionString());
// Create a database and collection
MongoDatabase database = mongoClient.getDatabase(dbname);
// Setup logger to capture log events
Logger logger = (Logger) LoggerFactory.getLogger(MongoDBPersistenceService.class);
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
logger.setLevel(Level.WARN);
// Return a SetupResult object containing the service, database, bundle context, config, item registry, and
// database name
return new SetupResult(service, database, bundleContext, config, itemRegistry, dbname);
}
/**
* Sets up a logger to capture log events.
*
* @param loggerClass The class that the logger is for.
* @param level The level of the logger.
* @return The list appender attached to the logger.
*/
public static ListAppender<ILoggingEvent> setupLogger(Class<?> loggerClass, Level level) {
Logger logger = (Logger) LoggerFactory.getLogger(loggerClass);
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
logger.setLevel(level); // Set log level
return listAppender;
}
private static Object convertValue(State state) {
Object value;
if (state instanceof PercentType) {
PercentType type = (PercentType) state;
value = type.toBigDecimal().doubleValue();
} else if (state instanceof DateTimeType) {
DateTimeType type = (DateTimeType) state;
value = Date.from(type.getZonedDateTime().toInstant());
} else if (state instanceof DecimalType) {
DecimalType type = (DecimalType) state;
value = type.toBigDecimal().doubleValue();
} else {
value = state.toString();
}
return value;
}
/**
* Stores the old data of an item into a MongoDB collection.
*
* @param collection The MongoDB collection where the data will be stored.
* @param realItemName The real name of the item.
* @param state The state of the item.
*/
public static void storeOldData(MongoCollection<Document> collection, String realItemName, State state) {
// use the old way to store data
Object value = convertValue(state);
Document obj = new Document();
obj.put(MongoDBFields.FIELD_ID, new ObjectId());
obj.put(MongoDBFields.FIELD_ITEM, realItemName);
obj.put(MongoDBFields.FIELD_REALNAME, realItemName);
obj.put(MongoDBFields.FIELD_TIMESTAMP, new Date());
obj.put(MongoDBFields.FIELD_VALUE, value);
collection.insertOne(obj);
}
public static List<PersistenceTestItem> createTestData(MongoDBPersistenceService service, String... itemNames) {
// Prepare a list to store the test data for verification
List<PersistenceTestItem> testDataList = new ArrayList<>();
// Prepare a random number generator
Random random = new Random();
// Prepare the start date
ZonedDateTime startDate = ZonedDateTime.now();
// Iterate over the 50 days
for (int day = 0; day < 50; day++) {
// Calculate the current date
ZonedDateTime currentDate = startDate.plusDays(day);
// Generate a random number of values for each item
for (String itemName : itemNames) {
int numValues = 2 + random.nextInt(4); // Random number between 2 and 5
for (int valueIndex = 0; valueIndex < numValues; valueIndex++) {
// Generate a random value between 0.0 and 10.0
double value = 10.0 * random.nextDouble();
// Create the item
Item item = DataCreationHelper.createNumberItem(itemName, value);
// Store the data
service.store(item, currentDate, new DecimalType(value));
// Add the data to the test data list for verification
testDataList.add(new PersistenceTestItem(itemName, currentDate, value));
}
}
}
return testDataList;
}
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.testcontainers.containers.MongoDBContainer;
import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
/**
* This class provides a container for MongoDB for testing purposes.
* It uses the Testcontainers library to manage the MongoDB container.
* It also provides an in-memory MongoDB server for testing.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public class DatabaseTestContainer {
// A map to store MongoDBContainer instances for different MongoDB versions.
private static final Map<String, MongoDBContainer> mongoDBContainers = new HashMap<>();
// The MongoDBContainer instance for this DatabaseTestContainer.
private @Nullable MongoDBContainer mongoDBContainer;
// The MongoServer instance for this DatabaseTestContainer.
private @Nullable MongoServer server;
// The InetSocketAddress instance for this DatabaseTestContainer.
private @Nullable InetSocketAddress serverAddress;
/**
* Creates a new DatabaseTestContainer for a given MongoDB version.
* If a MongoDBContainer for the given version already exists, it is reused.
*
* @param mongoDBVersion The version of MongoDB to use.
*/
public DatabaseTestContainer(String mongoDBVersion) {
server = null;
serverAddress = null;
mongoDBContainer = mongoDBContainers.computeIfAbsent(mongoDBVersion, MongoDBContainer::new);
}
/**
* Creates a new DatabaseTestContainer for an in-memory MongoDB server.
*/
public DatabaseTestContainer(MemoryBackend memoryBackend) {
mongoDBContainer = null;
server = new MongoServer(memoryBackend);
if (server != null) {
serverAddress = server.bind();
}
}
/**
* Starts the MongoDB container or the in-memory MongoDB server.
*/
public void start() {
if (mongoDBContainer != null && !mongoDBContainer.isRunning()) {
mongoDBContainer.start();
}
}
/**
* Don't do anything.
*/
public void stop() {
}
/**
* Returns the connection string for connecting to the MongoDB container or the in-memory MongoDB server.
*
* @return The connection string.
*/
public String getConnectionString() {
@Nullable
MongoDBContainer lc_mongoDBContainer = this.mongoDBContainer;
@Nullable
InetSocketAddress lc_serverAddress = this.serverAddress;
@Nullable
MongoServer lc_server = this.server;
if (lc_mongoDBContainer != null) {
return lc_mongoDBContainer.getConnectionString();
} else if (lc_server != null && lc_serverAddress != null) {
return String.format("mongodb://%s:%s", lc_serverAddress.getHostName(), lc_serverAddress.getPort());
} else {
return "";
}
}
}

View File

@ -0,0 +1,992 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.text.DateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.ImageItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.HistoricItem;
import org.osgi.framework.BundleContext;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
/**
* This is the implementation of the test for MongoDB {@link PersistenceService}.
*
* @author René Ulbricht - Initial contribution
*/
public class MongoDBPersistenceServiceTest {
/**
* Tests the activate method of MongoDBPersistenceService.
*
* This test checks if the activate method correctly logs the MongoDB URL, database, and collection.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testActivate(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
// Set up logger
ListAppender<ILoggingEvent> listAppender = DataCreationHelper.setupLogger(MongoDBPersistenceService.class,
Level.DEBUG);
// Execution
setupResult.service.activate(setupResult.bundleContext, setupResult.config);
// Verification
List<ILoggingEvent> logsList = listAppender.list;
VerificationHelper.verifyLogMessage(logsList.get(0), "MongoDB URL " + dbContainer.getConnectionString(),
Level.DEBUG);
VerificationHelper.verifyLogMessage(logsList.get(1), "MongoDB database " + setupResult.dbname, Level.DEBUG);
VerificationHelper.verifyLogMessage(logsList.get(2), "MongoDB collection testCollection", Level.DEBUG);
} finally {
dbContainer.stop();
}
}
/**
* Tests the deactivate method of MongoDBPersistenceService.
*
* This test checks if the deactivate method correctly logs a message when the MongoDB persistence bundle is
* stopping.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testDeactivate(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
setupResult.service.activate(setupResult.bundleContext, setupResult.config);
// Set up logger
ListAppender<ILoggingEvent> listAppender = DataCreationHelper.setupLogger(MongoDBPersistenceService.class,
Level.DEBUG);
// Execution
setupResult.service.deactivate(1);
// Verification
List<ILoggingEvent> logsList = listAppender.list;
VerificationHelper.verifyLogMessage(logsList.get(0),
"MongoDB persistence bundle stopping. Disconnecting from database.", Level.DEBUG);
} finally {
dbContainer.stop();
}
}
/**
* Tests the getId method of MongoDBPersistenceService.
*
* This test checks if the getId method correctly returns the ID of the MongoDBPersistenceService, which should be
* "mongodb".
*/
@Test
public void testGetId() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB(null, dbContainer);
MongoDBPersistenceService service = setupResult.service;
// Execution
String id = service.getId();
// Verification
assertEquals("mongodb", id);
} finally {
dbContainer.stop();
}
}
/**
* Tests the getLabel method of MongoDBPersistenceService.
*
* This test checks if the getLabel method correctly returns the label of the MongoDBPersistenceService, which
* should be "MongoDB".
*/
@Test
public void testGetLabel() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB(null, dbContainer);
MongoDBPersistenceService service = setupResult.service;
// Execution
String label = service.getLabel(null);
// Verification
assertEquals("MongoDB", label);
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of MongoDBPersistenceService with a NumberItem.
*
* This test checks if the store method correctly stores a NumberItem in the MongoDB database.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testStoreNumber(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
NumberItem item = DataCreationHelper.createNumberItem("TestItem", 10.1);
// Execution
service.store(item, null);
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocument(insertedDocument, "TestItem", 10.1);
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of MongoDBPersistenceService with a StringItem.
*
* This test checks if the store method correctly stores a StringItem in the MongoDB database.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testStoreString(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
StringItem item = DataCreationHelper.createStringItem("TestItem", "TestValue");
// Execution
service.store(item, null);
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocument(insertedDocument, "TestItem", "TestValue");
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of MongoDBPersistenceService with multiple items in a single collection.
*
* This test checks if the store method correctly stores multiple items in the same MongoDB collection.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testStoreSingleCollection(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
StringItem strItem1 = DataCreationHelper.createStringItem("TestItem", "TestValue");
StringItem strItem2 = DataCreationHelper.createStringItem("SecondTestItem", "SecondTestValue");
// Execution
service.store(strItem1, null);
service.store(strItem2, null);
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(2, documents.size()); // Assert that there are two documents
Document insertedDocument1 = documents.get(0); // Get the first document
VerificationHelper.verifyDocument(insertedDocument1, "TestItem", "TestValue");
Document insertedDocument2 = documents.get(1); // Get the second document
VerificationHelper.verifyDocument(insertedDocument2, "SecondTestItem", "SecondTestValue");
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of MongoDBPersistenceService with multiple items.
*
* This test checks if the store method correctly stores multiple items in the same MongoDB collection.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testStoreMultipleItemsSingleCollection(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
StringItem strItem1 = DataCreationHelper.createStringItem("TestItem1", "TestValue1");
StringItem strItem2 = DataCreationHelper.createStringItem("TestItem2", "TestValue2");
// Execution
service.store(strItem1, null);
service.store(strItem2, null);
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(2, documents.size()); // Assert that there are two documents
Document insertedDocument1 = documents.get(0); // Get the first document
VerificationHelper.verifyDocument(insertedDocument1, "TestItem1", "TestValue1");
Document insertedDocument2 = documents.get(1); // Get the second document
VerificationHelper.verifyDocument(insertedDocument2, "TestItem2", "TestValue2");
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of MongoDBPersistenceService with a StringItem and an alias.
*
* This test checks if the store method correctly stores a StringItem with an alias in the MongoDB database.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testStoreStringWithAlias(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
StringItem item = DataCreationHelper.createStringItem("TestItem", "TestValue");
// Execution
service.store(item, "AliasName");
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocumentWithAlias(insertedDocument, "AliasName", "TestItem", "TestValue");
} finally {
dbContainer.stop();
}
}
/**
* Tests the query method of MongoDBPersistenceService with NumberItems in a single collection.
*
* This test checks if the query method correctly retrieves NumberItems from a single MongoDB collection.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testQueryNumberItemsInOneCollection(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
// Add items to the ItemRegistry
NumberItem itemReg1 = DataCreationHelper.createNumberItem("TestItem", 0);
NumberItem itemReg2 = DataCreationHelper.createNumberItem("TestItem2", 0);
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem")).thenReturn(itemReg1);
Mockito.when(setupResult.itemRegistry.getItem("TestItem2")).thenReturn(itemReg2);
} catch (ItemNotFoundException e) {
}
service.activate(setupResult.bundleContext, setupResult.config);
// Store some items
for (int i = 0; i < 10; i++) {
NumberItem item1 = DataCreationHelper.createNumberItem("TestItem", i);
NumberItem item2 = DataCreationHelper.createNumberItem("TestItem2", i * 2);
service.store(item1, null);
service.store(item2, null);
}
// Execution
FilterCriteria filter1 = DataCreationHelper.createFilterCriteria("TestItem");
Iterable<HistoricItem> result1 = service.query(filter1);
FilterCriteria filter2 = DataCreationHelper.createFilterCriteria("TestItem2");
Iterable<HistoricItem> result2 = service.query(filter2);
// Verification
VerificationHelper.verifyQueryResult(result1, 0, 1, 10);
VerificationHelper.verifyQueryResult(result2, 0, 2, 10);
} finally {
dbContainer.stop();
}
}
/**
* Tests the query method of MongoDBPersistenceService with NumberItems in multiple collections.
*
* This test checks if the query method correctly retrieves NumberItems from multiple MongoDB collections.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testQueryNumberItemsInMultipleCollections(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB(null, dbContainer);
MongoDBPersistenceService service = setupResult.service;
BundleContext bundleContext = setupResult.bundleContext;
Map<String, Object> config = setupResult.config;
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem", 0));
Mockito.when(setupResult.itemRegistry.getItem("TestItem2"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem2", 0));
} catch (ItemNotFoundException e) {
}
service.activate(bundleContext, config);
// Store some items
for (int i = 0; i < 10; i++) {
NumberItem item1 = DataCreationHelper.createNumberItem("TestItem", i);
NumberItem item2 = DataCreationHelper.createNumberItem("TestItem2", i * 2);
service.store(item1, null);
service.store(item2, null);
}
// Execution
FilterCriteria filter1 = DataCreationHelper.createFilterCriteria("TestItem");
Iterable<HistoricItem> result1 = service.query(filter1);
FilterCriteria filter2 = DataCreationHelper.createFilterCriteria("TestItem2");
Iterable<HistoricItem> result2 = service.query(filter2);
// Verification
VerificationHelper.verifyQueryResult(result1, 0, 1, 10);
VerificationHelper.verifyQueryResult(result2, 0, 2, 10);
} finally {
dbContainer.stop();
}
}
/**
* Tests the query method of MongoDBPersistenceService with NumberItems in a single collection and a time range.
*
* This test checks if the query method correctly retrieves NumberItems from a single MongoDB collection within a
* specified time range.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testQueryNumberItemsInOneCollectionTimeRange(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem", 0));
Mockito.when(setupResult.itemRegistry.getItem("TestItem2"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem2", 0));
} catch (ItemNotFoundException e) {
}
service.activate(setupResult.bundleContext, setupResult.config);
// Get the collection
MongoCollection<Document> collection = database.getCollection("testCollection");
// Store items directly to the database with defined timestamps
for (int i = 0; i < 10; i++) {
Document obj = DataCreationHelper.createDocument("TestItem", i, LocalDate.now().minusDays(i));
collection.insertOne(obj);
Document obj2 = DataCreationHelper.createDocument("TestItem2", i * 2, LocalDate.now().minusDays(i));
collection.insertOne(obj2);
}
// Execution
FilterCriteria filter1 = DataCreationHelper.createFilterCriteria("TestItem",
ZonedDateTime.now().minusDays(5), null);
Iterable<HistoricItem> result1 = service.query(filter1);
// Verification
VerificationHelper.verifyQueryResult(result1, 4, -1, 5);
} finally {
dbContainer.stop();
}
}
/**
* Tests the query method of MongoDBPersistenceService with NumberItems in a single collection and a state equals
* filter.
*
* This test checks if the query method correctly retrieves NumberItems from a single MongoDB collection that match
* a specified state.
* It uses different database backends provided by the provideDatabaseBackends method.
*
* @param dbContainer The container running the MongoDB instance.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideDatabaseBackends")
public void testQueryNumberItemsInOneCollectionStateEquals(DatabaseTestContainer dbContainer) {
try {
// Preparation
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem", 0));
} catch (ItemNotFoundException e) {
}
service.activate(setupResult.bundleContext, setupResult.config);
// Get the collection
MongoCollection<Document> collection = database.getCollection("testCollection");
// Store items directly to the database with defined timestamps
for (int i = 0; i < 10; i++) {
Document obj = DataCreationHelper.createDocument("TestItem", i, LocalDate.now().minusDays(i));
collection.insertOne(obj);
}
Document obj = DataCreationHelper.createDocument("TestItem", 4.0, LocalDate.now());
collection.insertOne(obj);
// Execution
FilterCriteria filter1 = DataCreationHelper.createFilterCriteria("TestItem", null, null);
filter1.setState(new DecimalType(4.0));
filter1.setOperator(FilterCriteria.Operator.EQ);
Iterable<HistoricItem> result1 = service.query(filter1);
// Verification
VerificationHelper.verifyQueryResult(result1, 4, 0, 2);
} finally {
dbContainer.stop();
}
}
/**
* Tests the store method of the MongoDBPersistenceService with all types of openHAB items.
* Each item is stored in the collection in the MongoDB database.
*
* @param item The item to store in the database.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideOpenhabItemTypes")
public void testStoreAllOpenhabItemTypesSingleCollection(GenericItem item) {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
// Execution
service.store(item, null);
// Verification
MongoCollection<Document> collection = database.getCollection("testCollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocument(insertedDocument, item.getName(), item.getState());
} finally {
dbContainer.stop();
}
}
/**
* Tests the store and query method for various image sizes of the MongoDBPersistenceService
* Each item is queried with the type from one collection in the MongoDB database.
*
* @param item The item to store in the database.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideOpenhabImageItemsInDifferentSizes")
public void testStoreAndQueryyLargerImages(ImageItem item) {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
service.activate(setupResult.bundleContext, setupResult.config);
try {
Mockito.when(setupResult.itemRegistry.getItem(item.getName())).thenReturn(item);
} catch (ItemNotFoundException e) {
}
try {
service.store(item, null);
} catch (org.bson.BsonMaximumSizeExceededException e) {
if (item.getName().equals("ImageItem20MB")) {
// this is expected
return;
} else {
throw e;
}
}
// Execution
FilterCriteria filter = DataCreationHelper.createFilterCriteria(item.getName());
Iterable<HistoricItem> result = service.query(filter);
// Verification
VerificationHelper.verifyQueryResult(result, item.getState());
} finally {
dbContainer.stop();
}
}
/**
* Tests the old way of storing data and query method of the MongoDBPersistenceService with all types of openHAB
* items.
* Each item is queried with the type from one collection in the MongoDB database.
*
* @param item The item to store in the database.
*/
@ParameterizedTest
@MethodSource("org.openhab.persistence.mongodb.internal.DataCreationHelper#provideOpenhabItemTypes")
public void testOldDataQueryAllOpenhabItemTypesSingleCollection(GenericItem item) {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
try {
Mockito.when(setupResult.itemRegistry.getItem(item.getName())).thenReturn(item);
} catch (ItemNotFoundException e) {
}
MongoCollection<Document> collection = database.getCollection("testCollection");
DataCreationHelper.storeOldData(collection, item.getName(), item.getState());
// after storing, we have to adjust the expected values for ImageItems, ColorItems as well as DateTimeItems
if (item instanceof ImageItem) {
item.setState(new RawType(new byte[0], "application/octet-stream"));
} else if (item instanceof ColorItem) {
item.setState(new HSBType("0,0,0"));
}
// Execution
FilterCriteria filter = DataCreationHelper.createFilterCriteria(item.getName());
Iterable<HistoricItem> result = service.query(filter);
// Verification
if (item instanceof DateTimeItem) {
// verify just the date part
assertEquals(((DateTimeType) item.getState()).getZonedDateTime().toLocalDate(),
((DateTimeType) result.iterator().next().getState()).getZonedDateTime().toLocalDate());
} else {
VerificationHelper.verifyQueryResult(result, item.getState());
}
} finally {
dbContainer.stop();
}
}
/**
* Tests the writting of NumberItems including units
* Each item should be written to the database with the unit information
*
* @param item The item to store in the database.
*/
@Test
public void testStoreNumberItemWithUnit() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
MongoCollection<Document> collection = database.getCollection("testCollection");
NumberItem item = DataCreationHelper.createNumberItem("Number:Energy", "TestItem",
new QuantityType<>("10.1 kWh"));
// Execution
service.store(item, null);
// Verification
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
assertEquals(10.1, insertedDocument.get(MongoDBFields.FIELD_VALUE));
assertEquals("kWh", insertedDocument.get(MongoDBFields.FIELD_UNIT));
} finally {
dbContainer.stop();
}
}
/**
* Tests the reading of NumberItems including units
* Each item should be written to the database with the unit information
*
* @param item The item to store in the database.
*/
@Test
public void testQueryNumberItemWithUnit() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testCollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
MongoCollection<Document> collection = database.getCollection("testCollection");
NumberItem item = DataCreationHelper.createNumberItem("Number:Energy", "TestItem",
new QuantityType<>("10.1 MWh"));
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem")).thenReturn(item);
} catch (ItemNotFoundException e) {
}
Document obj = new Document();
obj.put(MongoDBFields.FIELD_ID, new ObjectId());
obj.put(MongoDBFields.FIELD_ITEM, "TestItem");
obj.put(MongoDBFields.FIELD_REALNAME, "TestItem");
obj.put(MongoDBFields.FIELD_TIMESTAMP, new Date());
obj.put(MongoDBFields.FIELD_VALUE, 201.5);
obj.put(MongoDBFields.FIELD_UNIT, "Wh");
collection.insertOne(obj);
// Execution
FilterCriteria filter = DataCreationHelper.createFilterCriteria("TestItem");
Iterable<HistoricItem> result = service.query(filter);
VerificationHelper.verifyQueryResult(result, new QuantityType<>("201.5 Wh"));
} finally {
dbContainer.stop();
}
}
/**
* Tests the toString of a MongoDBItem
*
*
* @param item The item to store in the database.
*/
@Test
public void testHistoricItemToString() {
// Preparation
ZonedDateTime now = ZonedDateTime.now();
HistoricItem item = new MongoDBItem("TestItem", new DecimalType(10.1), now);
// Execution
String result = item.toString();
// Verification
// Jan 29, 2024, 8:43:26 PM: TestItem -> 10.1
String expected = DateFormat.getDateTimeInstance().format(Date.from(now.toInstant())) + ": TestItem -> 10.1";
assertEquals(expected, result);
}
/*
* Test the store method which stores a item state as well as a timestampe (ZonedDateTime) and check the result in
* the database
*/
@Test
public void testStoreItemWithTimestamp() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB(null, dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem", 0));
} catch (ItemNotFoundException e) {
}
// Execution
NumberItem item = DataCreationHelper.createNumberItem("TestItem", 10.1);
DecimalType historicState = new DecimalType(11110.1);
ZonedDateTime now = ZonedDateTime.now();
service.store(item, now, historicState);
// Verification
MongoCollection<Document> collection = database.getCollection("TestItem");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocument(insertedDocument, "TestItem", historicState);
assertEquals(Date.from(now.toInstant()), insertedDocument.get(MongoDBFields.FIELD_TIMESTAMP));
} finally {
dbContainer.stop();
}
}
/*
* Test the store method which stores a item state as well as a timestampe (ZonedDateTime) and check the result in
* the database
*/
@Test
public void testStoreItemWithTimestampAndAlias() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB(null, dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
try {
Mockito.when(setupResult.itemRegistry.getItem("TestItem"))
.thenReturn(DataCreationHelper.createNumberItem("TestItem", 0));
} catch (ItemNotFoundException e) {
}
// Execution
NumberItem item = DataCreationHelper.createNumberItem("TestItem", 10.1);
DecimalType historicState = new DecimalType(11110.1);
ZonedDateTime now = ZonedDateTime.now();
service.store(item, now, historicState, "AliasName");
// Verification
MongoCollection<Document> collection = database.getCollection("TestItem");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is only one document
Document insertedDocument = documents.get(0); // Get the first (and only) document
VerificationHelper.verifyDocumentWithAlias(insertedDocument, "AliasName", "TestItem", historicState);
assertEquals(Date.from(now.toInstant()), insertedDocument.get(MongoDBFields.FIELD_TIMESTAMP));
} finally {
dbContainer.stop();
}
}
/*
* Test the remove method to remove one item from the database
*/
@Test
public void testremoveOneItem() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testcollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
for (double i = 0; i < 10.00; i += 0.3) {
service.store(DataCreationHelper.createNumberItem("TestItem", i));
}
service.store(DataCreationHelper.createNumberItem("TestItemOther", 10.1));
// Execution
service.remove(DataCreationHelper.createFilterCriteria("TestItem", null, null));
// Verification
MongoCollection<Document> collection = database.getCollection("testcollection");
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
assertEquals(1, documents.size()); // Assert that there is the other document
VerificationHelper.verifyDocument(documents.get(0), "TestItemOther", 10.1);
} finally {
dbContainer.stop();
}
}
/*
* Test the remove method to remove values of a given timerange for one item
*/
@Test
public void testremoveATimeRangeFromOneItem() {
// Preparation
DatabaseTestContainer dbContainer = new DatabaseTestContainer(new MemoryBackend());
try {
SetupResult setupResult = DataCreationHelper.setupMongoDB("testcollection", dbContainer);
MongoDBPersistenceService service = setupResult.service;
MongoDatabase database = setupResult.database;
service.activate(setupResult.bundleContext, setupResult.config);
List<PersistenceTestItem> testDataList = DataCreationHelper.createTestData(service, "TestItem",
"TestItemOther");
// Execution
// Calculate the start and end dates
ZonedDateTime startDate = ZonedDateTime.now().plusDays(3).truncatedTo(ChronoUnit.DAYS);
ZonedDateTime endDate = ZonedDateTime.now().plusDays(17).truncatedTo(ChronoUnit.DAYS).plusDays(1)
.minusNanos(1);
// Create the filter and remove the data
service.remove(DataCreationHelper.createFilterCriteria("TestItem", startDate, endDate));
// Verification
MongoCollection<Document> collection = database.getCollection("testcollection");
// Query the database for all data points
List<Document> documents = (ArrayList<Document>) collection.find().into(new ArrayList<>());
// Create a set of the returned data points
Set<PersistenceTestItem> returnedData = documents.stream()
.map(doc -> new PersistenceTestItem(doc.getString(MongoDBFields.FIELD_ITEM),
ZonedDateTime.ofInstant(doc.getDate(MongoDBFields.FIELD_TIMESTAMP).toInstant(),
ZoneId.systemDefault()),
doc.getDouble(MongoDBFields.FIELD_VALUE)))
.collect(Collectors.toSet());
// Create a set of the expected data points
Set<PersistenceTestItem> expectedData = testDataList
.stream().filter(testData -> !(testData.itemName.equals("TestItem")
&& testData.date.isAfter(startDate) && testData.date.isBefore(endDate)))
.collect(Collectors.toSet());
for (PersistenceTestItem expectedItem : expectedData) {
// Assert that this item is in the returned data
assertTrue(returnedData.contains(expectedItem),
"Expected item not found in returned data: " + expectedItem);
}
// Iterate over the returned data
for (PersistenceTestItem returnedItem : returnedData) {
// Assert that this item is in the expected data
assertTrue(expectedData.contains(returnedItem),
"Unexpected item found in returned data: " + returnedItem);
}
} finally {
dbContainer.stop();
}
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
/**
* This class provides helper methods to store generated data for persistence tests.
*
* @author René Ulbricht - Initial contribution
*/
public class PersistenceTestItem {
public final String itemName;
public final ZonedDateTime date;
public final double value;
public PersistenceTestItem(String itemName, ZonedDateTime date, double value) {
this.itemName = itemName;
this.date = date.truncatedTo(ChronoUnit.MILLIS);
this.value = value;
}
@Override
public String toString() {
return "PersistenceTestItem{" + "item='" + itemName + '\'' + ", date=" + date + ", value=" + value + '}';
}
@Override
public int hashCode() {
return Objects.hash(itemName, date, value);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PersistenceTestItem other = (PersistenceTestItem) obj;
return other.itemName.equals(itemName) && other.date.equals(date) && other.value == value;
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.items.ItemRegistry;
import org.osgi.framework.BundleContext;
import com.mongodb.client.MongoDatabase;
/**
* This class provides helper methods to create test items.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public class SetupResult {
public MongoDBPersistenceService service;
public MongoDatabase database;
public BundleContext bundleContext;
public Map<String, Object> config;
public ItemRegistry itemRegistry;
public String dbname;
public SetupResult(MongoDBPersistenceService service, MongoDatabase database, BundleContext bundleContext,
Map<String, Object> config, ItemRegistry itemRegistry, String dbname) {
this.service = service;
this.database = database;
this.dbname = dbname;
this.bundleContext = bundleContext;
this.config = config;
this.itemRegistry = itemRegistry;
}
}

View File

@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import org.apache.commons.lang3.tuple.Pair;
import org.bson.Document;
import org.bson.json.JsonWriterSettings;
import org.bson.types.Binary;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.persistence.HistoricItem;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* This is a helper class for verifying various aspects of the MongoDB persistence service.
* It provides methods for verifying log messages, MongoDB documents, and query results.
* Each verification method checks if the actual value matches the expected value and throws an
* AssertionError if they do not match.
*
* @author René Ulbricht - Initial contribution
*/
@NonNullByDefault
public class VerificationHelper {
/**
* Verifies a log message.
*
* @param logEvent The log event to verify.
* @param expectedMessage The expected message of the log event.
* @param expectedLevel The expected level of the log event.
*/
public static void verifyLogMessage(ILoggingEvent logEvent, String expectedMessage, Level expectedLevel) {
assertEquals(expectedMessage, logEvent.getFormattedMessage());
assertEquals(expectedLevel, logEvent.getLevel());
}
/**
* Verifies a document.
*
* @param document The document to verify.
* @param expectedItem The expected item of the document.
* @param expectedValue The expected value of the document.
*/
public static void verifyDocument(Document document, String expectedItem, Object expectedValue) {
verifyDocumentWithAlias(document, expectedItem, expectedItem, expectedValue);
}
/**
* Verifies a document with an alias.
*
* @param document The document to verify.
* @param expectedAlias The expected alias of the document.
* @param expectedRealName The expected real name of the document.
* @param expectedValue The expected value of the document. Can be a String or a Double.
*/
public static void verifyDocumentWithAlias(Document document, String expectedAlias, String expectedRealName,
Object expectedValue) {
assertEquals(expectedAlias, document.get(MongoDBFields.FIELD_ITEM));
assertEquals(expectedRealName, document.get(MongoDBFields.FIELD_REALNAME));
// Use the map to handle the expected value
BiFunction<Object, Document, Pair<Object, Object>> handler = HandleTypes.get(expectedValue.getClass());
if (handler == null) {
throw new IllegalArgumentException("Unsupported type: " + expectedValue.getClass());
}
Pair<Object, Object> values = handler.apply(expectedValue, document);
JsonWriterSettings jsonWriterSettings = JsonWriterSettings.builder().indent(true).build();
assertEquals(values.getLeft(), values.getRight(),
"Document: (" + expectedValue.getClass().getSimpleName() + ") " + document.toJson(jsonWriterSettings));
assertNotNull(document.get("_id"));
assertNotNull(document.get("timestamp"));
}
/**
* Verifies the result of a query.
*
* @param result The result of the query.
* @param startState The state of the first item in the result.
* @param increment The increment for the expected state.
*/
public static void verifyQueryResult(Iterable<HistoricItem> result, int startState, int increment, int totalSize) {
List<HistoricItem> resultList = new ArrayList<>();
result.forEach(resultList::add);
assertEquals(totalSize, resultList.size());
int expectedState = startState;
for (HistoricItem item : resultList) {
assertEquals(expectedState, ((DecimalType) item.getState()).intValue());
expectedState += increment;
}
}
public static void verifyQueryResult(Iterable<HistoricItem> result, Object expectedState) {
List<HistoricItem> resultList = new ArrayList<>();
result.forEach(resultList::add);
assertEquals(1, resultList.size());
assertEquals(expectedState, resultList.get(0).getState());
}
// Define a map from types to functions that handle those types
private static final Map<Class<?>, BiFunction<Object, Document, Pair<Object, Object>>> HandleTypes = Map.ofEntries(
Map.entry(Double.class, VerificationHelper::handleGeneric),
Map.entry(String.class, VerificationHelper::handleGeneric),
Map.entry(HSBType.class, VerificationHelper::handleToString),
Map.entry(DecimalType.class, VerificationHelper::handleDecimalType),
Map.entry(DateTimeType.class, VerificationHelper::handleDateTimeType),
Map.entry(IncreaseDecreaseType.class, VerificationHelper::handleToString),
Map.entry(RewindFastforwardType.class, VerificationHelper::handleToString),
Map.entry(NextPreviousType.class, VerificationHelper::handleToString),
Map.entry(OnOffType.class, VerificationHelper::handleToString),
Map.entry(OpenClosedType.class, VerificationHelper::handleToString),
Map.entry(PercentType.class, VerificationHelper::handlePercentType),
Map.entry(PlayPauseType.class, VerificationHelper::handleToString),
Map.entry(PointType.class, VerificationHelper::handleToString),
Map.entry(StopMoveType.class, VerificationHelper::handleToString),
Map.entry(StringListType.class, VerificationHelper::handleToString),
Map.entry(StringType.class, VerificationHelper::handleGeneric),
Map.entry(UpDownType.class, VerificationHelper::handleToString),
Map.entry(QuantityType.class, VerificationHelper::handleQuantityType),
Map.entry(RawType.class, VerificationHelper::handleRawType));
private static Pair<Object, Object> handleGeneric(Object ev, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
return Pair.of(ev, value != null ? value : new Object());
}
private static Pair<Object, Object> handleToString(Object ev, Document doc) {
Object value = doc.get(MongoDBFields.FIELD_VALUE);
return Pair.of(ev.toString(), value != null ? value : new Object());
}
private static Pair<Object, Object> handleDecimalType(Object ev, Document doc) {
Double value = doc.getDouble(MongoDBFields.FIELD_VALUE);
return Pair.of(((DecimalType) ev).doubleValue(), value != null ? value : new Object());
}
private static Pair<Object, Object> handleDateTimeType(Object ev, Document doc) {
String value = doc.getString(MongoDBFields.FIELD_VALUE);
return Pair.of(((DateTimeType) ev).getZonedDateTime().toString(), value != null ? value : new Object());
}
private static Pair<Object, Object> handlePercentType(Object ev, Document doc) {
Integer value = doc.getInteger(MongoDBFields.FIELD_VALUE);
return Pair.of(((PercentType) ev).intValue(), value != null ? value : new Object());
}
private static Pair<Object, Object> handleQuantityType(Object ev, Document doc) {
Double value = doc.getDouble(MongoDBFields.FIELD_VALUE);
String unit = doc.getString(MongoDBFields.FIELD_UNIT);
if (value != null && unit != null) {
QuantityType<?> quantityType = (QuantityType<?>) ev;
return Pair.of(quantityType.doubleValue() + "--" + quantityType.getUnit(), value + "--" + unit);
}
return Pair.of(new Object(), new Object());
}
private static Pair<Object, Object> handleRawType(Object ev, Document doc) {
RawType rawType = (RawType) ev;
Document expectedDoc = new Document();
expectedDoc.put(MongoDBFields.FIELD_VALUE_TYPE, rawType.getMimeType());
expectedDoc.put(MongoDBFields.FIELD_VALUE_DATA, new Binary(rawType.getBytes()));
Object value = doc.get(MongoDBFields.FIELD_VALUE);
return Pair.of(expectedDoc, value != null ? value : new Object());
}
}