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.mapdb.internal;
16 import java.io.IOException;
17 import java.nio.file.DirectoryStream;
18 import java.nio.file.Files;
19 import java.nio.file.Path;
20 import java.time.Instant;
21 import java.util.Date;
22 import java.util.List;
23 import java.util.Locale;
25 import java.util.Optional;
27 import java.util.concurrent.ExecutorService;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
34 import org.mapdb.DBMaker;
35 import org.openhab.core.OpenHAB;
36 import org.openhab.core.common.ThreadPoolManager;
37 import org.openhab.core.items.Item;
38 import org.openhab.core.persistence.FilterCriteria;
39 import org.openhab.core.persistence.HistoricItem;
40 import org.openhab.core.persistence.PersistenceItemInfo;
41 import org.openhab.core.persistence.PersistenceService;
42 import org.openhab.core.persistence.QueryablePersistenceService;
43 import org.openhab.core.persistence.strategy.PersistenceStrategy;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.osgi.service.component.annotations.Activate;
47 import org.osgi.service.component.annotations.Component;
48 import org.osgi.service.component.annotations.Deactivate;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
53 import com.google.gson.GsonBuilder;
56 * This is the implementation of the MapDB {@link PersistenceService}. To learn more about MapDB please visit their
57 * <a href="http://www.mapdb.org/">website</a>.
59 * @author Jens Viebig - Initial contribution
60 * @author Martin Kühl - Port to 3.x
63 @Component(service = { PersistenceService.class, QueryablePersistenceService.class })
64 public class MapDbPersistenceService implements QueryablePersistenceService {
66 private static final String SERVICE_ID = "mapdb";
67 private static final String SERVICE_LABEL = "MapDB";
68 private static final Path DB_DIR = new File(OpenHAB.getUserDataFolder(), "persistence").toPath().resolve("mapdb");
69 private static final Path BACKUP_DIR = DB_DIR.resolve("backup");
70 private static final String DB_FILE_NAME = "storage.mapdb";
72 private final Logger logger = LoggerFactory.getLogger(MapDbPersistenceService.class);
74 private final ExecutorService threadPool = ThreadPoolManager.getPool(getClass().getSimpleName());
76 /** holds the local instance of the MapDB database */
78 private @NonNullByDefault({}) DB db;
79 private @NonNullByDefault({}) Map<String, String> map;
81 private transient Gson mapper = new GsonBuilder().registerTypeHierarchyAdapter(State.class, new StateTypeAdapter())
85 public void activate() {
86 logger.debug("MapDB persistence service is being activated");
89 Files.createDirectories(DB_DIR);
90 } catch (IOException e) {
91 logger.warn("Failed to create one or more directories in the path '{}'", DB_DIR);
92 logger.warn("MapDB persistence service activation has failed.");
96 File dbFile = DB_DIR.resolve(DB_FILE_NAME).toFile();
98 db = DBMaker.newFileDB(dbFile).closeOnJvmShutdown().make();
99 map = db.createTreeMap("itemStore").makeOrGet();
100 } catch (RuntimeException re) {
101 Throwable cause = re.getCause();
102 if (cause instanceof ClassNotFoundException) {
103 ClassNotFoundException cnf = (ClassNotFoundException) cause;
105 "The MapDB in {} is incompatible with openHAB {}: {}. A new and empty MapDB will be used instead.",
106 dbFile, OpenHAB.getVersion(), cnf.getMessage());
109 Files.createDirectories(BACKUP_DIR);
110 } catch (IOException ioe) {
111 logger.warn("Failed to create one or more directories in the path '{}'", BACKUP_DIR);
112 logger.warn("MapDB persistence service activation has failed.");
116 try (DirectoryStream<Path> stream = Files.newDirectoryStream(DB_DIR)) {
117 long epochMilli = Instant.now().toEpochMilli();
118 for (Path path : stream) {
119 if (!Files.isDirectory(path)) {
120 Path newPath = BACKUP_DIR.resolve(epochMilli + "--" + path.getFileName());
121 Files.move(path, newPath);
122 logger.info("Moved incompatible MapDB file '{}' to '{}'", path, newPath);
125 } catch (IOException ioe) {
126 logger.warn("Failed to read files from '{}': {}", DB_DIR, ioe.getMessage());
127 logger.warn("MapDB persistence service activation has failed.");
131 db = DBMaker.newFileDB(dbFile).closeOnJvmShutdown().make();
132 map = db.createTreeMap("itemStore").makeOrGet();
134 logger.warn("Failed to create or open the MapDB: {}", re.getMessage());
135 logger.warn("MapDB persistence service activation has failed.");
138 logger.debug("MapDB persistence service is now activated");
142 public void deactivate() {
143 logger.debug("MapDB persistence service deactivated");
150 public String getId() {
155 public String getLabel(@Nullable Locale locale) {
156 return SERVICE_LABEL;
160 public Set<PersistenceItemInfo> getItemInfo() {
161 return map.values().stream().map(this::deserialize).flatMap(MapDbPersistenceService::streamOptional)
162 .collect(Collectors.<PersistenceItemInfo> toUnmodifiableSet());
166 public void store(Item item) {
167 store(item, item.getName());
171 public void store(Item item, @Nullable String alias) {
172 if (item.getState() instanceof UnDefType) {
176 // PersistenceManager passes SimpleItemConfiguration.alias which can be null
177 String localAlias = alias == null ? item.getName() : alias;
178 logger.debug("store called for {}", localAlias);
180 State state = item.getState();
181 MapDbItem mItem = new MapDbItem();
182 mItem.setName(localAlias);
183 mItem.setState(state);
184 mItem.setTimestamp(new Date());
185 String json = serialize(mItem);
186 map.put(localAlias, json);
188 if (logger.isDebugEnabled()) {
189 logger.debug("Stored '{}' with state '{}' as '{}' in MapDB database", localAlias, state, json);
194 public Iterable<HistoricItem> query(FilterCriteria filter) {
195 String json = map.get(filter.getItemName());
199 Optional<MapDbItem> item = deserialize(json);
200 return item.isPresent() ? List.of(item.get()) : List.of();
203 private String serialize(MapDbItem item) {
204 return mapper.toJson(item);
207 @SuppressWarnings("null")
208 private Optional<MapDbItem> deserialize(String json) {
209 MapDbItem item = mapper.<MapDbItem> fromJson(json, MapDbItem.class);
210 if (item == null || !item.isValid()) {
211 logger.warn("Deserialized invalid item: {}", item);
212 return Optional.empty();
213 } else if (logger.isDebugEnabled()) {
214 logger.debug("Deserialized '{}' with state '{}' from '{}'", item.getName(), item.getState(), json);
217 return Optional.of(item);
220 private void commit() {
221 threadPool.submit(() -> db.commit());
224 private static <T> Stream<T> streamOptional(Optional<T> opt) {
225 return opt.isPresent() ? Stream.of(opt.get()) : Stream.empty();
229 public List<PersistenceStrategy> getDefaultStrategies() {
230 return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE);