]> git.basschouten.com Git - openhab-addons.git/blob
8564a7b6e9b974346adafe7b8ddc4b6e0acfeabc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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         try {
177             return persistItem.database().stream().filter(e -> applies(e, filter)).map(e -> toHistoricItem(itemName, e))
178                     .toList();
179         } finally {
180             lock.unlock();
181         }
182     }
183
184     @Override
185     public List<PersistenceStrategy> getDefaultStrategies() {
186         // persist nothing by default
187         return List.of();
188     }
189
190     private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
191         Lock lock = itemEntry.getValue().lock();
192         lock.lock();
193         try {
194             String name = itemEntry.getKey();
195             Integer count = itemEntry.getValue().database().size();
196             Instant earliest = itemEntry.getValue().database().first().timestamp().toInstant();
197             Instant latest = itemEntry.getValue().database.last().timestamp.toInstant();
198             return new PersistenceItemInfo() {
199
200                 @Override
201                 public String getName() {
202                     return name;
203                 }
204
205                 @Override
206                 public @Nullable Integer getCount() {
207                     return count;
208                 }
209
210                 @Override
211                 public @Nullable Date getEarliest() {
212                     return Date.from(earliest);
213                 }
214
215                 @Override
216                 public @Nullable Date getLatest() {
217                     return Date.from(latest);
218                 }
219             };
220         } finally {
221             lock.unlock();
222         }
223     }
224
225     private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
226         return new HistoricItem() {
227             @Override
228             public ZonedDateTime getTimestamp() {
229                 return entry.timestamp();
230             }
231
232             @Override
233             public State getState() {
234                 return entry.state();
235             }
236
237             @Override
238             public String getName() {
239                 return itemName;
240             }
241         };
242     }
243
244     private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
245         if (state instanceof UnDefType) {
246             return;
247         }
248
249         PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
250                 k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
251                         new ReentrantLock())));
252
253         Lock lock = persistItem.lock();
254         lock.lock();
255         try {
256             persistItem.database().add(new PersistEntry(timestamp, state));
257
258             while (persistItem.database.size() > maxEntries) {
259                 persistItem.database().pollFirst();
260             }
261         } finally {
262             lock.unlock();
263         }
264     }
265
266     @SuppressWarnings({ "rawType", "unchecked" })
267     private boolean applies(PersistEntry entry, FilterCriteria filter) {
268         ZonedDateTime beginDate = filter.getBeginDate();
269         if (beginDate != null && entry.timestamp().isBefore(beginDate)) {
270             return false;
271         }
272         ZonedDateTime endDate = filter.getEndDate();
273         if (endDate != null && entry.timestamp().isAfter(endDate)) {
274             return false;
275         }
276
277         State refState = filter.getState();
278         FilterCriteria.Operator operator = filter.getOperator();
279         if (refState == null) {
280             // no state filter
281             return true;
282         }
283
284         if (operator == FilterCriteria.Operator.EQ) {
285             return entry.state().equals(refState);
286         }
287
288         if (operator == FilterCriteria.Operator.NEQ) {
289             return !entry.state().equals(refState);
290         }
291
292         if (entry.state() instanceof Comparable comparableState && entry.state.getClass().equals(refState.getClass())) {
293             if (operator == FilterCriteria.Operator.GT) {
294                 return comparableState.compareTo(refState) > 0;
295             }
296             if (operator == FilterCriteria.Operator.GTE) {
297                 return comparableState.compareTo(refState) >= 0;
298             }
299             if (operator == FilterCriteria.Operator.LT) {
300                 return comparableState.compareTo(refState) < 0;
301             }
302             if (operator == FilterCriteria.Operator.LTE) {
303                 return comparableState.compareTo(refState) <= 0;
304             }
305         } else {
306             logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
307         }
308         return true;
309     }
310
311     private record PersistEntry(ZonedDateTime timestamp, State state) {
312     };
313
314     private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {
315     };
316 }