/**
* Copyright (c) 2010-2016 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.openhab.persistence.mapdb.internal;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.repeatSecondlyForever;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.mapdb.DB;
import org.mapdb.DBMaker;
import org.mapdb.Serializer;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.BundleContext;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the implementation of the MapDB {@link PersistenceService}. To learn
* more about MapDB please visit their <a
* href="http://www.mapdb.org/">website</a>.
*
* @author Jens Viebig
* @since 1.7.0
*/
public class MapDBPersistenceService implements QueryablePersistenceService {
private static final String SERVICE_NAME = "mapdb";
protected final static String DB_FOLDER_NAME = getUserDataFolder() + File.separator + "mapdb";
private static final String DB_FILE_NAME = "storage.mapdb";
private static final String SCHEDULER_GROUP = "MapDB_SchedulerGroup";
private static int commitInterval = 5;
private static boolean commitSameState = false;
private static boolean needsCommit = false;
private static final Logger logger = LoggerFactory.getLogger(MapDBPersistenceService.class);
/** holds the local instance of the MapDB database */
private static DB db;
private static Map<String, MapDBItem> map;
public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
logger.debug("mapdb persistence service is being activated");
String commitIntervalString = (String) config.get("commitinterval");
if (StringUtils.isNotBlank(commitIntervalString)) {
try {
commitInterval = Integer.valueOf(commitIntervalString);
} catch (IllegalArgumentException iae) {
logger.warn("couldn't parse '{}' to an integer");
}
}
String commitSameStateString = (String) config.get("commitsamestate");
if (StringUtils.isNotBlank(commitSameStateString)) {
try {
commitSameState = Boolean.valueOf(commitSameStateString);
} catch (IllegalArgumentException iae) {
logger.warn("couldn't parse '{}' to an integer");
}
}
File folder = new File(DB_FOLDER_NAME);
if (!folder.exists()) {
if (!folder.mkdirs()) {
logger.error("Failed to create one or more directories in the path '{}'", DB_FOLDER_NAME);
logger.error("MapDB persistence service activation has failed.");
return;
}
}
File dbFile = new File(DB_FOLDER_NAME, DB_FILE_NAME);
db = DBMaker.newFileDB(dbFile).closeOnJvmShutdown().make();
Serializer<MapDBItem> serializer = new MapDBitemSerializer();
map = db.createTreeMap("itemStore").valueSerializer(serializer).makeOrGet();
scheduleJob();
logger.debug("mapdb persistence service is now activated");
}
public void deactivate(final int reason) {
logger.debug("mapdb persistence service deactivated");
if (db != null) {
db.close();
}
cancelAllJobs();
}
@Override
public String getName() {
return SERVICE_NAME;
}
@Override
public void store(Item item) {
store(item, null);
}
@Override
public void store(Item item, String alias) {
if (item.getState() instanceof UnDefType) {
return;
}
if (alias == null) {
alias = item.getName();
}
logger.debug("store called for {}", alias);
State state = item.getState();
if (item instanceof ColorItem) {
state = item.getStateAs(HSBType.class);
} else if (item instanceof DimmerItem || item instanceof RollershutterItem) {
state = item.getStateAs(PercentType.class);
}
MapDBItem mItem = new MapDBItem();
mItem.setName(alias);
mItem.setState(state);
mItem.setTimestamp(new Date());
MapDBItem oldItem = map.put(alias, mItem);
if (!commitSameState) {
if (oldItem != null) {
if (!oldItem.getState().toString().equals(state.toString())) {
needsCommit = true;
}
}
}
logger.debug("Stored '{}' with state '{}' in mapdb database", alias, state.toString());
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
HistoricItem item = map.get(filter.getItemName());
if (item != null) {
return Collections.singletonList(item);
}
return Collections.emptyList();
}
/**
* Schedules new quartz scheduler jobs for committing transactions and
* backing up the database
*/
private void scheduleJob() {
try {
Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
// schedule commit-job
JobDetail job = newJob(CommitJob.class).withIdentity("Commit_Transaction", SCHEDULER_GROUP).build();
SimpleTrigger trigger = newTrigger().withIdentity("Commit_Transaction", SCHEDULER_GROUP)
.withSchedule(repeatSecondlyForever(commitInterval)).build();
sched.scheduleJob(job, trigger);
logger.debug("Scheduled Commit-Job with interval {}sec.", commitInterval);
} catch (SchedulerException e) {
logger.warn("Could not create Job: {}", e.getMessage());
}
}
/**
* Delete all quartz scheduler jobs of the group <code>Dropbox</code>.
*/
private void cancelAllJobs() {
try {
Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
Set<JobKey> jobKeys = sched.getJobKeys(jobGroupEquals(SCHEDULER_GROUP));
if (jobKeys.size() > 0) {
sched.deleteJobs(new ArrayList<JobKey>(jobKeys));
logger.debug("Found {} MapDB-Jobs to delete from DefaultScheduler (keys={})", jobKeys.size(), jobKeys);
}
} catch (SchedulerException e) {
logger.warn("Couldn't remove Commit-Job: {}", e.getMessage());
}
}
/**
* A quartz scheduler job to commit the mapdb transaction frequently. There
* can be only one instance of a specific job type running at the same time.
*
* @author Jens Viebig
* @since 1.7.0
*/
@DisallowConcurrentExecution
public static class CommitJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
long startTime = System.currentTimeMillis();
try {
if (!db.isClosed() && (needsCommit || commitSameState)) {
needsCommit = false;
db.commit();
logger.trace("successfully commited mapdb transaction in {}ms",
System.currentTimeMillis() - startTime);
}
} catch (Exception e) {
try {
logger.warn("Error committing transaction : {}", e.getMessage());
if (!db.isClosed()) {
db.rollback();
}
} catch (Exception re) {
logger.debug("Rollback Exception: {}", e.getMessage());
}
}
}
}
private static String getUserDataFolder() {
String progArg = System.getProperty("smarthome.userdata");
if (progArg != null) {
return progArg + File.separator + "persistence";
} else {
return "etc";
}
}
}