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.inmemory.internal;
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;
22 import java.util.Objects;
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;
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;
52 * This is the implementation of the volatile {@link PersistenceService}.
54 * @author Jan N. Klug - Initial contribution
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 {
63 private static final String SERVICE_ID = "inmemory";
64 private static final String SERVICE_LABEL = "In Memory";
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;
70 private final Logger logger = LoggerFactory.getLogger(InMemoryPersistenceService.class);
72 private final Map<String, PersistItem> persistMap = new ConcurrentHashMap<>();
73 private long maxEntries = MAX_ENTRIES_DEFAULT;
76 public void activate(Map<String, Object> config) {
78 logger.debug("InMemory persistence service is now activated.");
82 public void modified(Map<String, Object> config) {
83 maxEntries = ConfigParser.valueAsOrElse(config.get(MAX_ENTRIES_CONFIG), Long.class, MAX_ENTRIES_DEFAULT);
85 persistMap.values().forEach(persistItem -> {
86 Lock lock = persistItem.lock();
89 while (persistItem.database().size() > maxEntries) {
90 persistItem.database().pollFirst();
99 public void deactivate() {
100 logger.debug("InMemory persistence service deactivated.");
104 public String getId() {
109 public String getLabel(@Nullable Locale locale) {
110 return SERVICE_LABEL;
114 public Set<PersistenceItemInfo> getItemInfo() {
115 return persistMap.entrySet().stream().map(this::toItemInfo).collect(Collectors.toSet());
119 public void store(Item item) {
120 internalStore(item.getName(), ZonedDateTime.now(), item.getState());
124 public void store(Item item, @Nullable String alias) {
125 String finalName = Objects.requireNonNullElse(alias, item.getName());
126 internalStore(finalName, ZonedDateTime.now(), item.getState());
130 public void store(Item item, ZonedDateTime date, State state) {
131 internalStore(item.getName(), date, state);
135 public void store(Item item, ZonedDateTime date, State state, @Nullable String alias) {
136 internalStore(Objects.requireNonNullElse(alias, item.getName()), date, state);
140 public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
141 String itemName = filter.getItemName();
142 if (itemName == null) {
146 PersistItem persistItem = persistMap.get(itemName);
147 if (persistItem == null) {
151 Lock lock = persistItem.lock();
154 List<PersistEntry> toRemove = persistItem.database().stream().filter(e -> applies(e, filter)).toList();
155 toRemove.forEach(persistItem.database()::remove);
163 public Iterable<HistoricItem> query(FilterCriteria filter) {
164 String itemName = filter.getItemName();
165 if (itemName == null) {
169 PersistItem persistItem = persistMap.get(itemName);
170 if (persistItem == null) {
174 Lock lock = persistItem.lock();
177 Comparator<PersistEntry> comparator = filter.getOrdering() == FilterCriteria.Ordering.ASCENDING
178 ? Comparator.comparing(PersistEntry::timestamp)
179 : Comparator.comparing(PersistEntry::timestamp).reversed();
182 return persistItem.database().stream().filter(e -> applies(e, filter)).sorted(comparator)
183 .map(e -> toHistoricItem(itemName, e)).toList();
190 public List<PersistenceStrategy> getDefaultStrategies() {
191 // persist nothing by default
195 private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
196 Lock lock = itemEntry.getValue().lock();
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() {
206 public String getName() {
211 public @Nullable Integer getCount() {
216 public @Nullable Date getEarliest() {
217 return Date.from(earliest);
221 public @Nullable Date getLatest() {
222 return Date.from(latest);
230 private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
231 return new HistoricItem() {
233 public ZonedDateTime getTimestamp() {
234 return entry.timestamp();
238 public State getState() {
239 return entry.state();
243 public String getName() {
249 private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
250 if (state instanceof UnDefType) {
254 PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
255 k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
256 new ReentrantLock())));
258 Lock lock = persistItem.lock();
261 persistItem.database().add(new PersistEntry(timestamp, state));
263 while (persistItem.database.size() > maxEntries) {
264 persistItem.database().pollFirst();
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)) {
277 ZonedDateTime endDate = filter.getEndDate();
278 if (endDate != null && entry.timestamp().isAfter(endDate)) {
282 State refState = filter.getState();
283 FilterCriteria.Operator operator = filter.getOperator();
284 if (refState == null) {
289 if (operator == FilterCriteria.Operator.EQ) {
290 return entry.state().equals(refState);
293 if (operator == FilterCriteria.Operator.NEQ) {
294 return !entry.state().equals(refState);
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;
301 if (operator == FilterCriteria.Operator.GTE) {
302 return comparableState.compareTo(refState) >= 0;
304 if (operator == FilterCriteria.Operator.LT) {
305 return comparableState.compareTo(refState) < 0;
307 if (operator == FilterCriteria.Operator.LTE) {
308 return comparableState.compareTo(refState) <= 0;
311 logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
316 private record PersistEntry(ZonedDateTime timestamp, State state) {
319 private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {