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 return persistItem.database().stream().filter(e -> applies(e, filter)).map(e -> toHistoricItem(itemName, e))
185 public List<PersistenceStrategy> getDefaultStrategies() {
186 // persist nothing by default
190 private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
191 Lock lock = itemEntry.getValue().lock();
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() {
201 public String getName() {
206 public @Nullable Integer getCount() {
211 public @Nullable Date getEarliest() {
212 return Date.from(earliest);
216 public @Nullable Date getLatest() {
217 return Date.from(latest);
225 private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
226 return new HistoricItem() {
228 public ZonedDateTime getTimestamp() {
229 return entry.timestamp();
233 public State getState() {
234 return entry.state();
238 public String getName() {
244 private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
245 if (state instanceof UnDefType) {
249 PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
250 k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
251 new ReentrantLock())));
253 Lock lock = persistItem.lock();
256 persistItem.database().add(new PersistEntry(timestamp, state));
258 while (persistItem.database.size() > maxEntries) {
259 persistItem.database().pollFirst();
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)) {
272 ZonedDateTime endDate = filter.getEndDate();
273 if (endDate != null && entry.timestamp().isAfter(endDate)) {
277 State refState = filter.getState();
278 FilterCriteria.Operator operator = filter.getOperator();
279 if (refState == null) {
284 if (operator == FilterCriteria.Operator.EQ) {
285 return entry.state().equals(refState);
288 if (operator == FilterCriteria.Operator.NEQ) {
289 return !entry.state().equals(refState);
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;
296 if (operator == FilterCriteria.Operator.GTE) {
297 return comparableState.compareTo(refState) >= 0;
299 if (operator == FilterCriteria.Operator.LT) {
300 return comparableState.compareTo(refState) < 0;
302 if (operator == FilterCriteria.Operator.LTE) {
303 return comparableState.compareTo(refState) <= 0;
306 logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
311 private record PersistEntry(ZonedDateTime timestamp, State state) {
314 private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {