]> git.basschouten.com Git - openhab-addons.git/blob
7461c9ecb5d82ee01a57a7f5503de58ace95c293
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.mapdb.internal;
14
15 import java.io.File;
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;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.Set;
27 import java.util.concurrent.ExecutorService;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.mapdb.DB;
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.framework.Constants;
47 import org.osgi.service.component.annotations.Activate;
48 import org.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.Deactivate;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import com.google.gson.Gson;
54 import com.google.gson.GsonBuilder;
55
56 /**
57  * This is the implementation of the MapDB {@link PersistenceService}. To learn more about MapDB please visit their
58  * <a href="http://www.mapdb.org/">website</a>.
59  *
60  * @author Jens Viebig - Initial contribution
61  * @author Martin Kühl - Port to 3.x
62  */
63 @NonNullByDefault
64 @Component(service = { PersistenceService.class, QueryablePersistenceService.class }, property = Constants.SERVICE_PID
65         + "=org.openhab.mapdb")
66 public class MapDbPersistenceService implements QueryablePersistenceService {
67
68     private static final String SERVICE_ID = "mapdb";
69     private static final String SERVICE_LABEL = "MapDB";
70     private static final Path DB_DIR = new File(OpenHAB.getUserDataFolder(), "persistence").toPath().resolve("mapdb");
71     private static final Path BACKUP_DIR = DB_DIR.resolve("backup");
72     private static final String DB_FILE_NAME = "storage.mapdb";
73
74     private final Logger logger = LoggerFactory.getLogger(MapDbPersistenceService.class);
75
76     private final ExecutorService threadPool = ThreadPoolManager.getPool(getClass().getSimpleName());
77
78     /**
79      * holds the local instance of the MapDB database
80      */
81
82     private @NonNullByDefault({}) DB db;
83     private @NonNullByDefault({}) Map<String, String> map;
84
85     private transient Gson mapper = new GsonBuilder().registerTypeHierarchyAdapter(State.class, new StateTypeAdapter())
86             .create();
87
88     @Activate
89     public void activate() {
90         logger.debug("MapDB persistence service is being activated");
91
92         try {
93             Files.createDirectories(DB_DIR);
94         } catch (IOException e) {
95             logger.warn("Failed to create one or more directories in the path '{}'", DB_DIR);
96             logger.warn("MapDB persistence service activation has failed.");
97             return;
98         }
99
100         File dbFile = DB_DIR.resolve(DB_FILE_NAME).toFile();
101         try {
102             db = DBMaker.newFileDB(dbFile).closeOnJvmShutdown().make();
103             map = db.createTreeMap("itemStore").makeOrGet();
104         } catch (RuntimeException re) {
105             Throwable cause = re.getCause();
106             if (cause instanceof ClassNotFoundException cnf) {
107                 logger.warn(
108                         "The MapDB in {} is incompatible with openHAB {}: {}. A new and empty MapDB will be used instead.",
109                         dbFile, OpenHAB.getVersion(), cnf.getMessage());
110
111                 try {
112                     Files.createDirectories(BACKUP_DIR);
113                 } catch (IOException ioe) {
114                     logger.warn("Failed to create one or more directories in the path '{}'", BACKUP_DIR);
115                     logger.warn("MapDB persistence service activation has failed.");
116                     return;
117                 }
118
119                 try (DirectoryStream<Path> stream = Files.newDirectoryStream(DB_DIR)) {
120                     long epochMilli = Instant.now().toEpochMilli();
121                     for (Path path : stream) {
122                         if (!Files.isDirectory(path)) {
123                             Path newPath = BACKUP_DIR.resolve(epochMilli + "--" + path.getFileName());
124                             Files.move(path, newPath);
125                             logger.info("Moved incompatible MapDB file '{}' to '{}'", path, newPath);
126                         }
127                     }
128                 } catch (IOException ioe) {
129                     logger.warn("Failed to read files from '{}': {}", DB_DIR, ioe.getMessage());
130                     logger.warn("MapDB persistence service activation has failed.");
131                     return;
132                 }
133
134                 db = DBMaker.newFileDB(dbFile).closeOnJvmShutdown().make();
135                 map = db.createTreeMap("itemStore").makeOrGet();
136             } else {
137                 logger.warn("Failed to create or open the MapDB: {}", re.getMessage());
138                 logger.warn("MapDB persistence service activation has failed.");
139             }
140         }
141         logger.debug("MapDB persistence service is now activated");
142     }
143
144     @Deactivate
145     public void deactivate() {
146         logger.debug("MapDB persistence service deactivated");
147         if (db != null) {
148             db.close();
149         }
150     }
151
152     @Override
153     public String getId() {
154         return SERVICE_ID;
155     }
156
157     @Override
158     public String getLabel(@Nullable Locale locale) {
159         return SERVICE_LABEL;
160     }
161
162     @Override
163     public Set<PersistenceItemInfo> getItemInfo() {
164         return map.values().stream().map(this::deserialize).flatMap(MapDbPersistenceService::streamOptional)
165                 .collect(Collectors.<PersistenceItemInfo> toUnmodifiableSet());
166     }
167
168     @Override
169     public void store(Item item) {
170         store(item, item.getName());
171     }
172
173     @Override
174     public void store(Item item, @Nullable String alias) {
175         if (item.getState() instanceof UnDefType) {
176             return;
177         }
178
179         // PersistenceManager passes SimpleItemConfiguration.alias which can be null
180         String localAlias = alias == null ? item.getName() : alias;
181         logger.debug("store called for {}", localAlias);
182
183         State state = item.getState();
184         MapDbItem mItem = new MapDbItem();
185         mItem.setName(localAlias);
186         mItem.setState(state);
187         mItem.setTimestamp(new Date());
188         threadPool.submit(() -> {
189             String json = serialize(mItem);
190             map.put(localAlias, json);
191             db.commit();
192             logger.debug("Stored '{}' with state '{}' as '{}' in MapDB database", localAlias, state, json);
193         });
194     }
195
196     @Override
197     public Iterable<HistoricItem> query(FilterCriteria filter) {
198         String json = map.get(filter.getItemName());
199         if (json == null) {
200             return List.of();
201         }
202         Optional<MapDbItem> item = deserialize(json);
203         return item.isPresent() ? List.of(item.get()) : List.of();
204     }
205
206     private String serialize(MapDbItem item) {
207         return mapper.toJson(item);
208     }
209
210     @SuppressWarnings("null")
211     private Optional<MapDbItem> deserialize(String json) {
212         MapDbItem item = mapper.fromJson(json, MapDbItem.class);
213         if (item == null || !item.isValid()) {
214             logger.warn("Deserialized invalid item: {}", item);
215             return Optional.empty();
216         } else if (logger.isDebugEnabled()) {
217             logger.debug("Deserialized '{}' with state '{}' from '{}'", item.getName(), item.getState(), json);
218         }
219
220         return Optional.of(item);
221     }
222
223     private static <T> Stream<T> streamOptional(Optional<T> opt) {
224         return opt.isPresent() ? Stream.of(opt.get()) : Stream.empty();
225     }
226
227     @Override
228     public List<PersistenceStrategy> getDefaultStrategies() {
229         return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE);
230     }
231 }