2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.mongodb.internal;
15 import static org.mockito.ArgumentMatchers.any;
16 import static org.mockito.Mockito.when;
18 import java.time.LocalDate;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Date;
22 import java.util.HashMap;
23 import java.util.Hashtable;
24 import java.util.List;
26 import java.util.Random;
27 import java.util.UUID;
28 import java.util.stream.Stream;
30 import org.bson.Document;
31 import org.bson.types.ObjectId;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.junit.jupiter.params.provider.Arguments;
35 import org.mockito.Mockito;
36 import org.openhab.core.i18n.UnitProvider;
37 import org.openhab.core.internal.i18n.I18nProviderImpl;
38 import org.openhab.core.items.GenericItem;
39 import org.openhab.core.items.Item;
40 import org.openhab.core.items.ItemRegistry;
41 import org.openhab.core.library.items.CallItem;
42 import org.openhab.core.library.items.ColorItem;
43 import org.openhab.core.library.items.ContactItem;
44 import org.openhab.core.library.items.DateTimeItem;
45 import org.openhab.core.library.items.DimmerItem;
46 import org.openhab.core.library.items.ImageItem;
47 import org.openhab.core.library.items.LocationItem;
48 import org.openhab.core.library.items.NumberItem;
49 import org.openhab.core.library.items.PlayerItem;
50 import org.openhab.core.library.items.RollershutterItem;
51 import org.openhab.core.library.items.StringItem;
52 import org.openhab.core.library.items.SwitchItem;
53 import org.openhab.core.library.types.DateTimeType;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.HSBType;
56 import org.openhab.core.library.types.OnOffType;
57 import org.openhab.core.library.types.OpenClosedType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.PlayPauseType;
60 import org.openhab.core.library.types.PointType;
61 import org.openhab.core.library.types.QuantityType;
62 import org.openhab.core.library.types.RawType;
63 import org.openhab.core.library.types.StringListType;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.library.unit.SIUnits;
66 import org.openhab.core.persistence.FilterCriteria;
67 import org.openhab.core.types.State;
68 import org.osgi.framework.BundleContext;
69 import org.osgi.service.component.ComponentContext;
70 import org.slf4j.LoggerFactory;
71 import org.testcontainers.DockerClientFactory;
73 import com.mongodb.client.MongoClient;
74 import com.mongodb.client.MongoClients;
75 import com.mongodb.client.MongoCollection;
76 import com.mongodb.client.MongoDatabase;
78 import ch.qos.logback.classic.Level;
79 import ch.qos.logback.classic.Logger;
80 import ch.qos.logback.classic.spi.ILoggingEvent;
81 import ch.qos.logback.core.read.ListAppender;
82 import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
85 * This class provides helper methods to create test items.
87 * @author René Ulbricht - Initial contribution
90 public class DataCreationHelper {
92 protected static final UnitProvider UNIT_PROVIDER;
94 ComponentContext context = Mockito.mock(ComponentContext.class);
95 BundleContext bundleContext = Mockito.mock(BundleContext.class);
96 Hashtable<String, Object> properties = new Hashtable<>();
97 properties.put("measurementSystem", SIUnits.MEASUREMENT_SYSTEM_NAME);
98 when(context.getProperties()).thenReturn(properties);
99 when(context.getBundleContext()).thenReturn(bundleContext);
100 UNIT_PROVIDER = new I18nProviderImpl(context);
104 * Creates a NumberItem with a given name and value.
106 * @param name The name of the NumberItem.
107 * @param value The value of the NumberItem.
108 * @return The created NumberItem.
110 public static NumberItem createNumberItem(String name, Number value) {
111 return createItem(NumberItem.class, name, new DecimalType(value));
115 * Creates a StringItem with a given name and value.
117 * @param name The name of the StringItem.
118 * @param value The value of the StringItem.
119 * @return The created StringItem.
121 public static StringItem createStringItem(String name, String value) {
122 return createItem(StringItem.class, name, new StringType(value));
126 * Creates an instance of a NumberItem with a unit type and sets its state.
128 * @param itemType The Class object representing the type of the item to create.
129 * @param unitType The string representation of the unit type to set on the new item.
130 * @param name The name to give to the new item.
131 * @param state The state to set on the new item.
132 * @return The newly created item.
133 * @throws RuntimeException if an error occurs while creating the item or setting its state.
135 public static NumberItem createNumberItem(String unitType, String name, State state) {
136 NumberItem item = new NumberItem(unitType, name, UNIT_PROVIDER);
137 item.setState(state);
142 * Creates an instance of a specific GenericItem subclass and sets its state.
144 * @param <T> The type of the item to create. This must be a subclass of GenericItem.
145 * @param <S> The type of the state to set. This must be a subclass of State.
146 * @param itemType The Class object representing the type of the item to create.
147 * @param name The name to give to the new item.
148 * @param state The state to set on the new item.
149 * @return The newly created item.
150 * @throws RuntimeException if an error occurs while creating the item or setting its state.
152 public static <T extends GenericItem, S extends State> T createItem(Class<T> itemType, String name, S state) {
155 throw new IllegalArgumentException("State must not be null");
157 T item = itemType.getDeclaredConstructor(String.class).newInstance(name);
159 throw new RuntimeException("Could not create item");
161 item.setState(state);
163 } catch (Exception e) {
164 throw new RuntimeException(e);
168 private static RawType createFakeImage(int size) {
169 byte[] data = new byte[size];
170 for (int i = 0; i < size; i++) {
171 data[i] = (byte) (i % 256);
173 return new RawType(data, "image/png");
177 * Provides a stream of arguments for parameterized tests. To test various image sizes
179 * @return A stream of arguments for parameterized tests.
181 public static Stream<Arguments> provideOpenhabImageItemsInDifferentSizes() {
183 Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem1kB", createFakeImage(1024))),
185 DataCreationHelper.createItem(ImageItem.class, "ImageItem1MB", createFakeImage(1024 * 1024))),
186 Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem10MB",
187 createFakeImage(10 * 1024 * 1024))),
188 Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem20MB",
189 createFakeImage(20 * 1024 * 1024))));
193 * Provides a stream of arguments for parameterized tests. Each argument is an instance of a specific
194 * GenericItem subclass with a set state.
196 * @return A stream of arguments for parameterized tests.
198 public static Stream<Arguments> provideOpenhabItemTypes() {
201 DataCreationHelper.createItem(StringItem.class, "StringItem", new StringType("StringValue"))),
202 Arguments.of(DataCreationHelper.createItem(NumberItem.class, "NumberItem", new DecimalType(123.45))),
203 Arguments.of(DataCreationHelper.createItem(DimmerItem.class, "DimmerItem", new PercentType(50))),
204 Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemON", OnOffType.ON)),
205 Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemOFF", OnOffType.OFF)),
206 Arguments.of(DataCreationHelper.createItem(ContactItem.class, "ContactItemOPEN", OpenClosedType.OPEN)),
208 DataCreationHelper.createItem(ContactItem.class, "ContactItemCLOSED", OpenClosedType.CLOSED)),
209 Arguments.of(DataCreationHelper.createItem(RollershutterItem.class, "RollershutterItem",
210 new PercentType(30))),
211 Arguments.of(DataCreationHelper.createItem(DateTimeItem.class, "DateTimeItem",
212 new DateTimeType(ZonedDateTime.now()))),
213 Arguments.of(DataCreationHelper.createItem(ColorItem.class, "ColorItem", new HSBType("180,100,100"))),
215 DataCreationHelper.createItem(LocationItem.class, "LocationItem", new PointType("51.0,0.0"))),
216 Arguments.of(DataCreationHelper.createItem(PlayerItem.class, "PlayerItem", PlayPauseType.PLAY)),
217 Arguments.of(DataCreationHelper.createItem(CallItem.class, "CallItem",
218 new StringListType("+49 123 456 789"))),
219 Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem",
220 new RawType(new byte[] { 0x00, 0x01, 0x02 }, "image/png"))),
221 Arguments.of(DataCreationHelper.createNumberItem("Number:Energy", "NumberItemCelcius",
222 new QuantityType<>("25.00 MWh"))),
223 Arguments.of(DataCreationHelper.createNumberItem("Number:Temperature", "NumberItemCelcius",
224 new QuantityType<>("25.00 °F"))));
228 * Provides a stream of arguments to be used for parameterized tests.
230 * Each argument is a DatabaseTestContainer instance. Some instances use a MemoryBackend,
231 * while others use a MongoDBContainer with a specific MongoDB version.
232 * In case there is no Docker available, only the MemoryBackend is used.
234 * @return A stream of Arguments, each containing a DatabaseTestContainer instance.
236 public static Stream<Arguments> provideDatabaseBackends() {
237 if (DockerClientFactory.instance().isDockerAvailable()) {
238 // If Docker is available, create a stream of Arguments with all backends
240 // Create a DatabaseTestContainer with a MemoryBackend
241 Arguments.of(new DatabaseTestContainer(new MemoryBackend())),
242 // Create DatabaseTestContainers with MongoDBContainers of specific versions
243 Arguments.of(new DatabaseTestContainer("mongo:3.6")),
244 Arguments.of(new DatabaseTestContainer("mongo:4.4")),
245 Arguments.of(new DatabaseTestContainer("mongo:5.0")),
246 Arguments.of(new DatabaseTestContainer("mongo:6.0")));
248 // If Docker is not available, create a stream of Arguments with only the MemoryBackend
249 return Stream.of(Arguments.of(new DatabaseTestContainer(new MemoryBackend())));
254 * Creates a Document for a given item name, value, and timestamp.
256 * @param itemName The name of the item.
257 * @param value The value of the item.
258 * @param timestamp The timestamp of the item.
259 * @return The created Document.
261 public static Document createDocument(String itemName, double value, LocalDate timestamp) {
262 Document obj = new Document();
263 obj.put(MongoDBFields.FIELD_ID, new ObjectId());
264 obj.put(MongoDBFields.FIELD_ITEM, itemName);
265 obj.put(MongoDBFields.FIELD_REALNAME, itemName);
266 obj.put(MongoDBFields.FIELD_TIMESTAMP, timestamp);
267 obj.put(MongoDBFields.FIELD_VALUE, value);
272 * Creates a FilterCriteria for a given item name.
274 * @param itemName The name of the item.
275 * @return The created FilterCriteria.
277 public static FilterCriteria createFilterCriteria(String itemName) {
278 return createFilterCriteria(itemName, null, null);
282 * Creates a FilterCriteria for a given item name, begin date, and end date.
284 * @param itemName The name of the item.
285 * @param beginDate The begin date of the FilterCriteria.
286 * @param endDate The end date of the FilterCriteria.
287 * @return The created FilterCriteria.
289 public static FilterCriteria createFilterCriteria(String itemName, @Nullable ZonedDateTime beginDate,
290 @Nullable ZonedDateTime endDate) {
291 FilterCriteria filter = new FilterCriteria();
292 filter.setItemName(itemName);
293 filter.setPageSize(10);
294 filter.setPageNumber(0);
295 filter.setOrdering(FilterCriteria.Ordering.ASCENDING);
296 if (beginDate != null) {
297 filter.setBeginDate(beginDate);
299 if (endDate != null) {
300 filter.setEndDate(endDate);
306 * Sets up a MongoDB instance for testing.
308 * @param collectionName The name of the MongoDB collection to be used for testing.
309 * @param dbContainer The container running the MongoDB instance.
310 * @return A SetupResult object containing the MongoDBPersistenceService, the database, the bundle context, the
311 * configuration map, the item registry, and the database name.
313 public static SetupResult setupMongoDB(@Nullable String collectionName, DatabaseTestContainer dbContainer) {
314 // Start the database container
317 // Mock the ItemRegistry and BundleContext
318 ItemRegistry itemRegistry = Mockito.mock(ItemRegistry.class);
319 BundleContext bundleContext = Mockito.mock(BundleContext.class);
321 // When getService is called on the bundleContext, return the mocked itemRegistry
322 when(bundleContext.getService(any())).thenReturn(itemRegistry);
324 // Create a new MongoDBPersistenceService instance
325 MongoDBPersistenceService service = new MongoDBPersistenceService(itemRegistry);
327 // Create a configuration map for the MongoDBPersistenceService
328 Map<String, Object> config = new HashMap<>();
329 config.put("url", dbContainer.getConnectionString());
330 String dbname = UUID.randomUUID().toString();
331 config.put("database", dbname);
332 if (collectionName != null) {
333 config.put("collection", collectionName);
336 // Create a MongoClient connected to the mock server
337 MongoClient mongoClient = MongoClients.create(dbContainer.getConnectionString());
339 // Create a database and collection
340 MongoDatabase database = mongoClient.getDatabase(dbname);
342 // Setup logger to capture log events
343 Logger logger = (Logger) LoggerFactory.getLogger(MongoDBPersistenceService.class);
344 ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
345 listAppender.start();
346 logger.addAppender(listAppender);
347 logger.setLevel(Level.WARN);
349 // Return a SetupResult object containing the service, database, bundle context, config, item registry, and
351 return new SetupResult(service, database, bundleContext, config, itemRegistry, dbname);
355 * Sets up a logger to capture log events.
357 * @param loggerClass The class that the logger is for.
358 * @param level The level of the logger.
359 * @return The list appender attached to the logger.
361 public static ListAppender<ILoggingEvent> setupLogger(Class<?> loggerClass, Level level) {
362 Logger logger = (Logger) LoggerFactory.getLogger(loggerClass);
363 ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
364 listAppender.start();
365 logger.addAppender(listAppender);
366 logger.setLevel(level); // Set log level
370 private static Object convertValue(State state) {
372 if (state instanceof PercentType) {
373 PercentType type = (PercentType) state;
374 value = type.toBigDecimal().doubleValue();
375 } else if (state instanceof DateTimeType) {
376 DateTimeType type = (DateTimeType) state;
377 value = Date.from(type.getZonedDateTime().toInstant());
378 } else if (state instanceof DecimalType) {
379 DecimalType type = (DecimalType) state;
380 value = type.toBigDecimal().doubleValue();
382 value = state.toString();
388 * Stores the old data of an item into a MongoDB collection.
390 * @param collection The MongoDB collection where the data will be stored.
391 * @param realItemName The real name of the item.
392 * @param state The state of the item.
394 public static void storeOldData(MongoCollection<Document> collection, String realItemName, State state) {
395 // use the old way to store data
396 Object value = convertValue(state);
398 Document obj = new Document();
399 obj.put(MongoDBFields.FIELD_ID, new ObjectId());
400 obj.put(MongoDBFields.FIELD_ITEM, realItemName);
401 obj.put(MongoDBFields.FIELD_REALNAME, realItemName);
402 obj.put(MongoDBFields.FIELD_TIMESTAMP, new Date());
403 obj.put(MongoDBFields.FIELD_VALUE, value);
404 collection.insertOne(obj);
407 public static List<PersistenceTestItem> createTestData(MongoDBPersistenceService service, String... itemNames) {
408 // Prepare a list to store the test data for verification
409 List<PersistenceTestItem> testDataList = new ArrayList<>();
411 // Prepare a random number generator
412 Random random = new Random();
414 // Prepare the start date
415 ZonedDateTime startDate = ZonedDateTime.now();
417 // Iterate over the 50 days
418 for (int day = 0; day < 50; day++) {
419 // Calculate the current date
420 ZonedDateTime currentDate = startDate.plusDays(day);
422 // Generate a random number of values for each item
423 for (String itemName : itemNames) {
424 int numValues = 2 + random.nextInt(4); // Random number between 2 and 5
426 for (int valueIndex = 0; valueIndex < numValues; valueIndex++) {
427 // Generate a random value between 0.0 and 10.0
428 double value = 10.0 * random.nextDouble();
431 Item item = DataCreationHelper.createNumberItem(itemName, value);
434 service.store(item, currentDate, new DecimalType(value));
436 // Add the data to the test data list for verification
437 testDataList.add(new PersistenceTestItem(itemName, currentDate, value));