]> git.basschouten.com Git - openhab-addons.git/blob
83ecd75b40c0721f0553ac5779acbdf750d9e70a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.mongodb.internal;
14
15 import static org.mockito.ArgumentMatchers.any;
16 import static org.mockito.Mockito.when;
17
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;
25 import java.util.Map;
26 import java.util.Random;
27 import java.util.UUID;
28 import java.util.stream.Stream;
29
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;
72
73 import com.mongodb.client.MongoClient;
74 import com.mongodb.client.MongoClients;
75 import com.mongodb.client.MongoCollection;
76 import com.mongodb.client.MongoDatabase;
77
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;
83
84 /**
85  * This class provides helper methods to create test items.
86  * 
87  * @author René Ulbricht - Initial contribution
88  */
89 @NonNullByDefault
90 public class DataCreationHelper {
91
92     protected static final UnitProvider UNIT_PROVIDER;
93     static {
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);
101     }
102
103     /**
104      * Creates a NumberItem with a given name and value.
105      * 
106      * @param name The name of the NumberItem.
107      * @param value The value of the NumberItem.
108      * @return The created NumberItem.
109      */
110     public static NumberItem createNumberItem(String name, Number value) {
111         return createItem(NumberItem.class, name, new DecimalType(value));
112     }
113
114     /**
115      * Creates a StringItem with a given name and value.
116      * 
117      * @param name The name of the StringItem.
118      * @param value The value of the StringItem.
119      * @return The created StringItem.
120      */
121     public static StringItem createStringItem(String name, String value) {
122         return createItem(StringItem.class, name, new StringType(value));
123     }
124
125     /**
126      * Creates an instance of a NumberItem with a unit type and sets its state.
127      *
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.
134      */
135     public static NumberItem createNumberItem(String unitType, String name, State state) {
136         NumberItem item = new NumberItem(unitType, name, UNIT_PROVIDER);
137         item.setState(state);
138         return item;
139     }
140
141     /**
142      * Creates an instance of a specific GenericItem subclass and sets its state.
143      *
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.
151      */
152     public static <T extends GenericItem, S extends State> T createItem(Class<T> itemType, String name, S state) {
153         try {
154             if (state == null) {
155                 throw new IllegalArgumentException("State must not be null");
156             }
157             T item = itemType.getDeclaredConstructor(String.class).newInstance(name);
158             if (item == null) {
159                 throw new RuntimeException("Could not create item");
160             }
161             item.setState(state);
162             return item;
163         } catch (Exception e) {
164             throw new RuntimeException(e);
165         }
166     }
167
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);
172         }
173         return new RawType(data, "image/png");
174     }
175
176     /**
177      * Provides a stream of arguments for parameterized tests. To test various image sizes
178      *
179      * @return A stream of arguments for parameterized tests.
180      */
181     public static Stream<Arguments> provideOpenhabImageItemsInDifferentSizes() {
182         return Stream.of(
183                 Arguments.of(DataCreationHelper.createItem(ImageItem.class, "ImageItem1kB", createFakeImage(1024))),
184                 Arguments.of(
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))));
190     }
191
192     /**
193      * Provides a stream of arguments for parameterized tests. Each argument is an instance of a specific
194      * GenericItem subclass with a set state.
195      *
196      * @return A stream of arguments for parameterized tests.
197      */
198     public static Stream<Arguments> provideOpenhabItemTypes() {
199         return Stream.of(
200                 Arguments.of(
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)),
207                 Arguments.of(
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"))),
214                 Arguments.of(
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"))));
225     }
226
227     /**
228      * Provides a stream of arguments to be used for parameterized tests.
229      *
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.
233      *
234      * @return A stream of Arguments, each containing a DatabaseTestContainer instance.
235      */
236     public static Stream<Arguments> provideDatabaseBackends() {
237         if (DockerClientFactory.instance().isDockerAvailable()) {
238             // If Docker is available, create a stream of Arguments with all backends
239             return Stream.of(
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")));
247         } else {
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())));
250         }
251     }
252
253     /**
254      * Creates a Document for a given item name, value, and timestamp.
255      *
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.
260      */
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);
268         return obj;
269     }
270
271     /**
272      * Creates a FilterCriteria for a given item name.
273      *
274      * @param itemName The name of the item.
275      * @return The created FilterCriteria.
276      */
277     public static FilterCriteria createFilterCriteria(String itemName) {
278         return createFilterCriteria(itemName, null, null);
279     }
280
281     /**
282      * Creates a FilterCriteria for a given item name, begin date, and end date.
283      *
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.
288      */
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);
298         }
299         if (endDate != null) {
300             filter.setEndDate(endDate);
301         }
302         return filter;
303     }
304
305     /**
306      * Sets up a MongoDB instance for testing.
307      *
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.
312      */
313     public static SetupResult setupMongoDB(@Nullable String collectionName, DatabaseTestContainer dbContainer) {
314         // Start the database container
315         dbContainer.start();
316
317         // Mock the ItemRegistry and BundleContext
318         ItemRegistry itemRegistry = Mockito.mock(ItemRegistry.class);
319         BundleContext bundleContext = Mockito.mock(BundleContext.class);
320
321         // When getService is called on the bundleContext, return the mocked itemRegistry
322         when(bundleContext.getService(any())).thenReturn(itemRegistry);
323
324         // Create a new MongoDBPersistenceService instance
325         MongoDBPersistenceService service = new MongoDBPersistenceService(itemRegistry);
326
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);
334         }
335
336         // Create a MongoClient connected to the mock server
337         MongoClient mongoClient = MongoClients.create(dbContainer.getConnectionString());
338
339         // Create a database and collection
340         MongoDatabase database = mongoClient.getDatabase(dbname);
341
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);
348
349         // Return a SetupResult object containing the service, database, bundle context, config, item registry, and
350         // database name
351         return new SetupResult(service, database, bundleContext, config, itemRegistry, dbname);
352     }
353
354     /**
355      * Sets up a logger to capture log events.
356      *
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.
360      */
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
367         return listAppender;
368     }
369
370     private static Object convertValue(State state) {
371         Object value;
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();
381         } else {
382             value = state.toString();
383         }
384         return value;
385     }
386
387     /**
388      * Stores the old data of an item into a MongoDB collection.
389      *
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.
393      */
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);
397
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);
405     }
406
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<>();
410
411         // Prepare a random number generator
412         Random random = new Random();
413
414         // Prepare the start date
415         ZonedDateTime startDate = ZonedDateTime.now();
416
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);
421
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
425
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();
429
430                     // Create the item
431                     Item item = DataCreationHelper.createNumberItem(itemName, value);
432
433                     // Store the data
434                     service.store(item, currentDate, new DecimalType(value));
435
436                     // Add the data to the test data list for verification
437                     testDataList.add(new PersistenceTestItem(itemName, currentDate, value));
438                 }
439             }
440         }
441
442         return testDataList;
443     }
444 }