/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.storage.mapdb;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.eclipse.smarthome.core.storage.Storage;
import org.mapdb.DB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The MapDbStorage is concrete implementation of the {@link Storage} interface.
* It stores the key-value pairs in files. This Storage serializes and deserializes
* the given values using their JSON representation (generated by {@code Gson}.
* This transformation should help maintaining version compatibility of the stored
* data.
*
* @author Thomas.Eichstaedt-Engelen - Initial Contribution and API
* @author Alex Tugarev - Loading with Class.forName() if classLoader is null
*/
public class MapDbStorage<T> implements Storage<T> {
private static final String TYPE_SEPARATOR = "@@@";
private final Logger logger = LoggerFactory.getLogger(MapDbStorage.class);
private DB db;
private ClassLoader classLoader;
private Map<String, String> map;
private transient Gson mapper;
public MapDbStorage(DB db, String name, ClassLoader classLoader) {
this.db = db;
this.classLoader = classLoader;
this.map = db.createTreeMap(name).makeOrGet();
this.mapper = new GsonBuilder().registerTypeAdapterFactory(new PropertiesTypeAdapterFactory()).create();
}
/**
* {@inheritDoc}
*/
@Override
public T put(String key, T value) {
String previousValue = map.put(key, serialize(value));
db.commit();
return deserialize(previousValue);
}
/**
* {@inheritDoc}
*/
@Override
public T remove(String key) {
String removedElement = map.remove(key);
db.commit();
return deserialize(removedElement);
}
/**
* {@inheritDoc}
*/
@Override
public T get(String key) {
return deserialize(map.get(key));
}
/**
* {@inheritDoc}
*/
@Override
public Collection<String> getKeys() {
return map.keySet();
}
/**
* {@inheritDoc}
*/
@Override
public Collection<T> getValues() {
Collection<T> values = new ArrayList<T>();
for (String key : getKeys()) {
values.add(get(key));
}
return values;
}
/**
* Transforms the given {@code value} into its JSON representation using {@code Gson}. Since we do not know the type
* of {@code value} while
* deserializing it afterwards we prepend its qualified type name to the
* JSON String.
*
* @param value the {@code value} to store
* @return the JSON document prepended with the qualified type name of {@code value}
*/
private String serialize(T value) {
if (value == null) {
throw new IllegalArgumentException("Cannot serialize NULL");
}
String valueTypeName = value.getClass().getName();
String valueAsString = mapper.toJson(value);
String concatValue = valueTypeName + TYPE_SEPARATOR + valueAsString;
logger.trace("serialized value '{}' to MapDB", concatValue);
return concatValue;
}
/**
* Deserializes and instantiates an object of type {@code T} out of the
* given JSON String. A special classloader (other than the one of the
* MapDB bundle) is used in order to load the classes in the context of
* the calling bundle.
*
* @param json
* @return
*/
@SuppressWarnings("unchecked")
public T deserialize(String json) {
if (json == null) {
// nothing to deserialize
return null;
}
String[] concatValue = json.split(TYPE_SEPARATOR);
String valueTypeName = concatValue[0];
String valueAsString = concatValue[1];
T value = null;
try {
// load required class within the given bundle context
Class<T> loadedValueType = null;
if (classLoader == null) {
loadedValueType = (Class<T>) Class.forName(valueTypeName);
} else {
loadedValueType = (Class<T>) classLoader.loadClass(valueTypeName);
}
value = mapper.fromJson(valueAsString, loadedValueType);
logger.trace("deserialized value '{}' from MapDB", value);
} catch (Exception e) {
logger.warn("Couldn't deserialize value '{}'. Root cause is: {}", json, e.getMessage());
}
return value;
}
}