]> git.basschouten.com Git - openhab-addons.git/blob
3b4ae0bac7322fb1260ca963a9cdb98850d02585
[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.inmemory.internal;
14
15 import java.time.Instant;
16 import java.time.ZonedDateTime;
17 import java.util.Comparator;
18 import java.util.Date;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Set;
24 import java.util.TreeSet;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.locks.Lock;
27 import java.util.concurrent.locks.ReentrantLock;
28 import java.util.stream.Collectors;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.core.config.core.ConfigParser;
33 import org.openhab.core.config.core.ConfigurableService;
34 import org.openhab.core.items.Item;
35 import org.openhab.core.persistence.FilterCriteria;
36 import org.openhab.core.persistence.HistoricItem;
37 import org.openhab.core.persistence.ModifiablePersistenceService;
38 import org.openhab.core.persistence.PersistenceItemInfo;
39 import org.openhab.core.persistence.PersistenceService;
40 import org.openhab.core.persistence.strategy.PersistenceStrategy;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.osgi.framework.Constants;
44 import org.osgi.service.component.annotations.Activate;
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Deactivate;
47 import org.osgi.service.component.annotations.Modified;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * This is the implementation of the volatile {@link PersistenceService}.
53  *
54  * @author Jan N. Klug - Initial contribution
55  */
56 @NonNullByDefault
57 @Component(service = { PersistenceService.class,
58         ModifiablePersistenceService.class }, configurationPid = "org.openhab.inmemory", //
59         property = Constants.SERVICE_PID + "=org.openhab.inmemory")
60 @ConfigurableService(category = "persistence", label = "InMemory Persistence Service", description_uri = InMemoryPersistenceService.CONFIG_URI)
61 public class InMemoryPersistenceService implements ModifiablePersistenceService {
62
63     private static final String SERVICE_ID = "inmemory";
64     private static final String SERVICE_LABEL = "In Memory";
65
66     protected static final String CONFIG_URI = "persistence:inmemory";
67     private final String MAX_ENTRIES_CONFIG = "maxEntries";
68     private final long MAX_ENTRIES_DEFAULT = 512;
69
70     private final Logger logger = LoggerFactory.getLogger(InMemoryPersistenceService.class);
71
72     private final Map<String, PersistItem> persistMap = new ConcurrentHashMap<>();
73     private long maxEntries = MAX_ENTRIES_DEFAULT;
74
75     @Activate
76     public void activate(Map<String, Object> config) {
77         modified(config);
78         logger.debug("InMemory persistence service is now activated.");
79     }
80
81     @Modified
82     public void modified(Map<String, Object> config) {
83         maxEntries = ConfigParser.valueAsOrElse(config.get(MAX_ENTRIES_CONFIG), Long.class, MAX_ENTRIES_DEFAULT);
84
85         persistMap.values().forEach(persistItem -> {
86             Lock lock = persistItem.lock();
87             lock.lock();
88             try {
89                 while (persistItem.database().size() > maxEntries) {
90                     persistItem.database().pollFirst();
91                 }
92             } finally {
93                 lock.unlock();
94             }
95         });
96     }
97
98     @Deactivate
99     public void deactivate() {
100         logger.debug("InMemory persistence service deactivated.");
101     }
102
103     @Override
104     public String getId() {
105         return SERVICE_ID;
106     }
107
108     @Override
109     public String getLabel(@Nullable Locale locale) {
110         return SERVICE_LABEL;
111     }
112
113     @Override
114     public Set<PersistenceItemInfo> getItemInfo() {
115         return persistMap.entrySet().stream().map(this::toItemInfo).collect(Collectors.toSet());
116     }
117
118     @Override
119     public void store(Item item) {
120         internalStore(item.getName(), ZonedDateTime.now(), item.getState());
121     }
122
123     @Override
124     public void store(Item item, @Nullable String alias) {
125         String finalName = Objects.requireNonNullElse(alias, item.getName());
126         internalStore(finalName, ZonedDateTime.now(), item.getState());
127     }
128
129     @Override
130     public void store(Item item, ZonedDateTime date, State state) {
131         internalStore(item.getName(), date, state);
132     }
133
134     @Override
135     public void store(Item item, ZonedDateTime date, State state, @Nullable String alias) {
136         internalStore(Objects.requireNonNullElse(alias, item.getName()), date, state);
137     }
138
139     @Override
140     public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
141         String itemName = filter.getItemName();
142         if (itemName == null) {
143             return false;
144         }
145
146         PersistItem persistItem = persistMap.get(itemName);
147         if (persistItem == null) {
148             return false;
149         }
150
151         Lock lock = persistItem.lock();
152         lock.lock();
153         try {
154             List<PersistEntry> toRemove = persistItem.database().stream().filter(e -> applies(e, filter)).toList();
155             toRemove.forEach(persistItem.database()::remove);
156         } finally {
157             lock.unlock();
158         }
159         return true;
160     }
161
162     @Override
163     public Iterable<HistoricItem> query(FilterCriteria filter) {
164         String itemName = filter.getItemName();
165         if (itemName == null) {
166             return List.of();
167         }
168
169         PersistItem persistItem = persistMap.get(itemName);
170         if (persistItem == null) {
171             return List.of();
172         }
173
174         Lock lock = persistItem.lock();
175         lock.lock();
176
177         Comparator<PersistEntry> comparator = filter.getOrdering() == FilterCriteria.Ordering.ASCENDING
178                 ? Comparator.comparing(PersistEntry::timestamp)
179                 : Comparator.comparing(PersistEntry::timestamp).reversed();
180
181         try {
182             return persistItem.database().stream().filter(e -> applies(e, filter)).sorted(comparator)
183                     .map(e -> toHistoricItem(itemName, e)).toList();
184         } finally {
185             lock.unlock();
186         }
187     }
188
189     @Override
190     public List<PersistenceStrategy> getDefaultStrategies() {
191         // persist nothing by default
192         return List.of();
193     }
194
195     private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
196         Lock lock = itemEntry.getValue().lock();
197         lock.lock();
198         try {
199             String name = itemEntry.getKey();
200             Integer count = itemEntry.getValue().database().size();
201             Instant earliest = itemEntry.getValue().database().first().timestamp().toInstant();
202             Instant latest = itemEntry.getValue().database.last().timestamp.toInstant();
203             return new PersistenceItemInfo() {
204
205                 @Override
206                 public String getName() {
207                     return name;
208                 }
209
210                 @Override
211                 public @Nullable Integer getCount() {
212                     return count;
213                 }
214
215                 @Override
216                 public @Nullable Date getEarliest() {
217                     return Date.from(earliest);
218                 }
219
220                 @Override
221                 public @Nullable Date getLatest() {
222                     return Date.from(latest);
223                 }
224             };
225         } finally {
226             lock.unlock();
227         }
228     }
229
230     private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
231         return new HistoricItem() {
232             @Override
233             public ZonedDateTime getTimestamp() {
234                 return entry.timestamp();
235             }
236
237             @Override
238             public State getState() {
239                 return entry.state();
240             }
241
242             @Override
243             public String getName() {
244                 return itemName;
245             }
246         };
247     }
248
249     private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
250         if (state instanceof UnDefType) {
251             return;
252         }
253
254         PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
255                 k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
256                         new ReentrantLock())));
257
258         Lock lock = persistItem.lock();
259         lock.lock();
260         try {
261             persistItem.database().add(new PersistEntry(timestamp, state));
262
263             while (persistItem.database.size() > maxEntries) {
264                 persistItem.database().pollFirst();
265             }
266         } finally {
267             lock.unlock();
268         }
269     }
270
271     @SuppressWarnings({ "rawType", "unchecked" })
272     private boolean applies(PersistEntry entry, FilterCriteria filter) {
273         ZonedDateTime beginDate = filter.getBeginDate();
274         if (beginDate != null && entry.timestamp().isBefore(beginDate)) {
275             return false;
276         }
277         ZonedDateTime endDate = filter.getEndDate();
278         if (endDate != null && entry.timestamp().isAfter(endDate)) {
279             return false;
280         }
281
282         State refState = filter.getState();
283         FilterCriteria.Operator operator = filter.getOperator();
284         if (refState == null) {
285             // no state filter
286             return true;
287         }
288
289         if (operator == FilterCriteria.Operator.EQ) {
290             return entry.state().equals(refState);
291         }
292
293         if (operator == FilterCriteria.Operator.NEQ) {
294             return !entry.state().equals(refState);
295         }
296
297         if (entry.state() instanceof Comparable comparableState && entry.state.getClass().equals(refState.getClass())) {
298             if (operator == FilterCriteria.Operator.GT) {
299                 return comparableState.compareTo(refState) > 0;
300             }
301             if (operator == FilterCriteria.Operator.GTE) {
302                 return comparableState.compareTo(refState) >= 0;
303             }
304             if (operator == FilterCriteria.Operator.LT) {
305                 return comparableState.compareTo(refState) < 0;
306             }
307             if (operator == FilterCriteria.Operator.LTE) {
308                 return comparableState.compareTo(refState) <= 0;
309             }
310         } else {
311             logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
312         }
313         return true;
314     }
315
316     private record PersistEntry(ZonedDateTime timestamp, State state) {
317     };
318
319     private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {
320     };
321 }