2 * Copyright (c) 2010-2023 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 boolean remove(FilterCriteria filter) throws IllegalArgumentException {
136 String itemName = filter.getItemName();
137 if (itemName == null) {
141 PersistItem persistItem = persistMap.get(itemName);
142 if (persistItem == null) {
146 Lock lock = persistItem.lock();
149 List<PersistEntry> toRemove = persistItem.database().stream().filter(e -> applies(e, filter)).toList();
150 toRemove.forEach(persistItem.database()::remove);
158 public Iterable<HistoricItem> query(FilterCriteria filter) {
159 String itemName = filter.getItemName();
160 if (itemName == null) {
164 PersistItem persistItem = persistMap.get(itemName);
165 if (persistItem == null) {
169 Lock lock = persistItem.lock();
172 return persistItem.database().stream().filter(e -> applies(e, filter)).map(e -> toHistoricItem(itemName, e))
180 public List<PersistenceStrategy> getDefaultStrategies() {
181 // persist nothing by default
185 private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
186 Lock lock = itemEntry.getValue().lock();
189 String name = itemEntry.getKey();
190 Integer count = itemEntry.getValue().database().size();
191 Instant earliest = itemEntry.getValue().database().first().timestamp().toInstant();
192 Instant latest = itemEntry.getValue().database.last().timestamp.toInstant();
193 return new PersistenceItemInfo() {
196 public String getName() {
201 public @Nullable Integer getCount() {
206 public @Nullable Date getEarliest() {
207 return Date.from(earliest);
211 public @Nullable Date getLatest() {
212 return Date.from(latest);
220 private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
221 return new HistoricItem() {
223 public ZonedDateTime getTimestamp() {
224 return entry.timestamp();
228 public State getState() {
229 return entry.state();
233 public String getName() {
239 private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
240 if (state instanceof UnDefType) {
244 PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
245 k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
246 new ReentrantLock())));
248 Lock lock = persistItem.lock();
251 persistItem.database().add(new PersistEntry(timestamp, state));
253 while (persistItem.database.size() > maxEntries) {
254 persistItem.database().pollFirst();
261 @SuppressWarnings({ "rawType", "unchecked" })
262 private boolean applies(PersistEntry entry, FilterCriteria filter) {
263 ZonedDateTime beginDate = filter.getBeginDate();
264 if (beginDate != null && entry.timestamp().isBefore(beginDate)) {
267 ZonedDateTime endDate = filter.getEndDate();
268 if (endDate != null && entry.timestamp().isAfter(endDate)) {
272 State refState = filter.getState();
273 FilterCriteria.Operator operator = filter.getOperator();
274 if (refState == null) {
279 if (operator == FilterCriteria.Operator.EQ) {
280 return entry.state().equals(refState);
283 if (operator == FilterCriteria.Operator.NEQ) {
284 return !entry.state().equals(refState);
287 if (entry.state() instanceof Comparable comparableState && entry.state.getClass().equals(refState.getClass())) {
288 if (operator == FilterCriteria.Operator.GT) {
289 return comparableState.compareTo(refState) > 0;
291 if (operator == FilterCriteria.Operator.GTE) {
292 return comparableState.compareTo(refState) >= 0;
294 if (operator == FilterCriteria.Operator.LT) {
295 return comparableState.compareTo(refState) < 0;
297 if (operator == FilterCriteria.Operator.LTE) {
298 return comparableState.compareTo(refState) <= 0;
301 logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
306 private record PersistEntry(ZonedDateTime timestamp, State state) {
309 private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {