/**
* 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.core.persistence.internal;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.smarthome.core.items.GenericItem;
import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.ItemNotFoundException;
import org.eclipse.smarthome.core.items.ItemRegistry;
import org.eclipse.smarthome.core.items.ItemRegistryChangeListener;
import org.eclipse.smarthome.core.items.StateChangeListener;
import org.eclipse.smarthome.core.persistence.FilterCriteria;
import org.eclipse.smarthome.core.persistence.HistoricItem;
import org.eclipse.smarthome.core.persistence.PersistenceManager;
import org.eclipse.smarthome.core.persistence.PersistenceService;
import org.eclipse.smarthome.core.persistence.PersistenceServiceConfiguration;
import org.eclipse.smarthome.core.persistence.QueryablePersistenceService;
import org.eclipse.smarthome.core.persistence.SimpleItemConfiguration;
import org.eclipse.smarthome.core.persistence.config.SimpleAllConfig;
import org.eclipse.smarthome.core.persistence.config.SimpleConfig;
import org.eclipse.smarthome.core.persistence.config.SimpleGroupConfig;
import org.eclipse.smarthome.core.persistence.config.SimpleItemConfig;
import org.eclipse.smarthome.core.persistence.strategy.SimpleCronStrategy;
import org.eclipse.smarthome.core.persistence.strategy.SimpleStrategy;
import org.eclipse.smarthome.core.scheduler.CronExpression;
import org.eclipse.smarthome.core.scheduler.ExpressionThreadPoolManager;
import org.eclipse.smarthome.core.scheduler.ExpressionThreadPoolManager.ExpressionThreadPoolExecutor;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements a persistence manager to manage all persistence services etc.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage.
*/
public class PersistenceManagerImpl implements PersistenceManager, ItemRegistryChangeListener, StateChangeListener {
private final Logger logger = LoggerFactory.getLogger(PersistenceManager.class);
// the scheduler used for timer events
private ExpressionThreadPoolExecutor scheduler;
private ItemRegistry itemRegistry;
final Map<String, PersistenceService> persistenceServices = new HashMap<>();
final Map<String, PersistenceServiceConfiguration> persistenceServiceConfigs = new HashMap<>();
private final Map<String, Set<Runnable>> persistenceJobs = new HashMap<>();
public PersistenceManagerImpl() {
}
protected void activate() {
scheduler = ExpressionThreadPoolManager.getExpressionScheduledPool("persist");
}
protected void deactivate() {
scheduler.shutdown();
scheduler = null;
}
protected void setItemRegistry(ItemRegistry itemRegistry) {
this.itemRegistry = itemRegistry;
itemRegistry.addRegistryChangeListener(this);
allItemsChanged(null);
}
protected void unsetItemRegistry(ItemRegistry itemRegistry) {
itemRegistry.removeRegistryChangeListener(this);
this.itemRegistry = null;
}
protected void addPersistenceService(PersistenceService persistenceService) {
logger.debug("Initializing {} persistence service.", persistenceService.getId());
persistenceServices.put(persistenceService.getId(), persistenceService);
stopEventHandling(persistenceService.getId());
startEventHandling(persistenceService.getId());
}
protected void removePersistenceService(PersistenceService persistenceService) {
stopEventHandling(persistenceService.getId());
persistenceServices.remove(persistenceService.getId());
}
/**
* Calls all persistence services which use change or update policy for the given item
*
* @param item the item to persist
* @param onlyChanges true, if it has the change strategy, false otherwise
*/
private void handleStateEvent(Item item, boolean onlyChanges) {
synchronized (persistenceServiceConfigs) {
for (Entry<String, PersistenceServiceConfiguration> entry : persistenceServiceConfigs.entrySet()) {
final String serviceName = entry.getKey();
final PersistenceServiceConfiguration config = entry.getValue();
if (persistenceServices.containsKey(serviceName)) {
for (SimpleItemConfiguration itemConfig : config.getConfigs()) {
if (hasStrategy(serviceName, itemConfig,
onlyChanges ? SimpleStrategy.Globals.CHANGE : SimpleStrategy.Globals.UPDATE)) {
if (appliesToItem(itemConfig, item)) {
persistenceServices.get(serviceName).store(item, itemConfig.getAlias());
}
}
}
}
}
}
}
/**
* Checks if a given persistence configuration entry has a certain strategy for the given service
*
* @param serviceName the service to check the configuration for
* @param itemConfig the persistence configuration entry
* @param strategy the strategy to check for
* @return true, if it has the given strategy
*/
private boolean hasStrategy(String serviceName, SimpleItemConfiguration itemConfig, SimpleStrategy strategy) {
final PersistenceServiceConfiguration config = persistenceServiceConfigs.get(serviceName);
if (config.getDefaults().contains(strategy) && itemConfig.getStrategies().isEmpty()) {
return true;
} else {
for (SimpleStrategy s : itemConfig.getStrategies()) {
if (s.equals(strategy)) {
return true;
}
}
return false;
}
}
/**
* Checks if a given persistence configuration entry is relevant for an item
*
* @param config the persistence configuration entry
* @param item to check if the configuration applies to
* @return true, if the configuration applies to the item
*/
private boolean appliesToItem(SimpleItemConfiguration config, Item item) {
for (SimpleConfig itemCfg : config.getItems()) {
if (itemCfg instanceof SimpleAllConfig) {
return true;
}
if (itemCfg instanceof SimpleItemConfig) {
SimpleItemConfig singleItemConfig = (SimpleItemConfig) itemCfg;
if (item.getName().equals(singleItemConfig.getItem())) {
return true;
}
}
if (itemCfg instanceof SimpleGroupConfig) {
SimpleGroupConfig groupItemCfg = (SimpleGroupConfig) itemCfg;
String groupName = groupItemCfg.getGroup();
try {
Item gItem = itemRegistry.getItem(groupName);
if (gItem instanceof GroupItem) {
GroupItem groupItem = (GroupItem) gItem;
if (groupItem.getAllMembers().contains(item)) {
return true;
}
}
} catch (Exception e) {
}
}
}
return false;
}
/**
* Retrieves all items for which the persistence configuration applies to.
*
* @param config the persistence configuration entry
* @return all items that this configuration applies to
*/
Iterable<Item> getAllItems(SimpleItemConfiguration config) {
// first check, if we should return them all
for (Object itemCfg : config.getItems()) {
if (itemCfg instanceof SimpleAllConfig) {
return itemRegistry.getItems();
}
}
// otherwise, go through the detailed definitions
Set<Item> items = new HashSet<Item>();
for (Object itemCfg : config.getItems()) {
if (itemCfg instanceof SimpleItemConfig) {
SimpleItemConfig singleItemConfig = (SimpleItemConfig) itemCfg;
try {
Item item = itemRegistry.getItem(singleItemConfig.getItem());
items.add(item);
} catch (ItemNotFoundException e) {
logger.debug("Item '{}' does not exist.", singleItemConfig.getItem());
}
}
if (itemCfg instanceof SimpleGroupConfig) {
SimpleGroupConfig groupItemCfg = (SimpleGroupConfig) itemCfg;
String groupName = groupItemCfg.getGroup();
try {
Item gItem = itemRegistry.getItem(groupName);
if (gItem instanceof GroupItem) {
GroupItem groupItem = (GroupItem) gItem;
items.addAll(groupItem.getAllMembers());
}
} catch (ItemNotFoundException e) {
logger.debug("Item group '{}' does not exist.", groupName);
}
}
}
return items;
}
/**
* Handles the "restoreOnStartup" strategy for the item.
* If the item state is still undefined when entering this method, all persistence configurations are checked,
* if they have the "restoreOnStartup" strategy configured for the item. If so, the item state will be set
* to its last persisted value.
*
* @param item the item to restore the state for
*/
private void initialize(Item item) {
// get the last persisted state from the persistence service if no state is yet set
if (item.getState().equals(UnDefType.NULL) && item instanceof GenericItem) {
for (Entry<String, PersistenceServiceConfiguration> entry : persistenceServiceConfigs.entrySet()) {
final String serviceName = entry.getKey();
final PersistenceServiceConfiguration config = entry.getValue();
for (SimpleItemConfiguration itemConfig : config.getConfigs()) {
if (hasStrategy(serviceName, itemConfig, SimpleStrategy.Globals.RESTORE)) {
if (appliesToItem(itemConfig, item)) {
PersistenceService service = persistenceServices.get(serviceName);
if (service instanceof QueryablePersistenceService) {
QueryablePersistenceService queryService = (QueryablePersistenceService) service;
FilterCriteria filter = new FilterCriteria().setItemName(item.getName()).setPageSize(1);
Iterable<HistoricItem> result = queryService.query(filter);
Iterator<HistoricItem> it = result.iterator();
if (it.hasNext()) {
HistoricItem historicItem = it.next();
GenericItem genericItem = (GenericItem) item;
genericItem.removeStateChangeListener(this);
genericItem.setState(historicItem.getState());
genericItem.addStateChangeListener(this);
logger.debug("Restored item state from '{}' for item '{}' -> '{}'",
new Object[] {
DateFormat.getDateTimeInstance()
.format(historicItem.getTimestamp()),
item.getName(), historicItem.getState().toString() });
return;
}
} else if (service != null) {
logger.warn(
"Failed to restore item states as persistence service '{}' can not be queried.",
serviceName);
}
}
}
}
}
}
}
/**
* Creates and schedules a new quartz-job.
*
* @param modelName the name of the model
* @param strategies a collection of strategies
*/
private void createTimers(final String modelName, List<SimpleStrategy> strategies) {
for (SimpleStrategy strategy : strategies) {
if (strategy instanceof SimpleCronStrategy) {
SimpleCronStrategy cronStrategy = (SimpleCronStrategy) strategy;
String cronExpression = cronStrategy.getCronExpression();
final CronExpression expression;
try {
expression = new CronExpression(cronExpression);
} catch (final ParseException ex) {
logger.warn("Cannot parse cron expression ({}).", cronExpression, ex);
continue;
}
final PersistItemsJob job = new PersistItemsJob(this, modelName, cronStrategy.getName());
if (persistenceJobs.containsKey(modelName)) {
persistenceJobs.get(modelName).add(job);
} else {
final Set<Runnable> jobs = new HashSet<>();
jobs.add(job);
persistenceJobs.put(modelName, jobs);
}
scheduler.schedule(job, expression);
logger.debug("Scheduled strategy {} with cron expression {}", cronStrategy.getName(), cronExpression);
}
}
}
/**
* Delete all {@link Job}s of the group <code>persistModelName</code>
*
* @throws SchedulerException if there is an internal Scheduler error.
*/
private void removeTimers(String persistModelName) {
if (!persistenceJobs.containsKey(persistModelName)) {
return;
}
for (final Runnable job : persistenceJobs.get(persistModelName)) {
boolean success = scheduler.remove(job);
if (success) {
logger.debug("Removed scheduled cron job for dbId '{}'", persistModelName);
} else {
logger.warn("Failed to delete cron job for dbId '{}'", persistModelName);
}
}
persistenceJobs.remove(persistModelName);
}
/*
* PersistenceManager
*/
@Override
public void addConfig(final String dbId, final PersistenceServiceConfiguration config) {
synchronized (persistenceServiceConfigs) {
this.persistenceServiceConfigs.put(dbId, config);
if (itemRegistry != null && persistenceServices.containsKey(dbId)) {
startEventHandling(dbId);
}
}
}
@Override
public void removeConfig(final String dbId) {
synchronized (persistenceServiceConfigs) {
stopEventHandling(dbId);
this.persistenceServiceConfigs.remove(dbId);
}
}
@Override
public void startEventHandling(final String dbId) {
synchronized (persistenceServiceConfigs) {
final PersistenceServiceConfiguration config = persistenceServiceConfigs.get(dbId);
if (config == null) {
return;
}
if (itemRegistry != null) {
for (SimpleItemConfiguration itemConfig : config.getConfigs()) {
if (hasStrategy(dbId, itemConfig, SimpleStrategy.Globals.RESTORE)) {
for (Item item : getAllItems(itemConfig)) {
initialize(item);
}
}
}
}
createTimers(dbId, config.getStrategies());
}
}
@Override
public void stopEventHandling(String modelName) {
synchronized (persistenceServiceConfigs) {
removeTimers(modelName);
}
}
/*
* ItemRegistryChangeListener
*/
@Override
public void allItemsChanged(Collection<String> oldItemNames) {
for (Item item : itemRegistry.getItems()) {
added(item);
}
}
@Override
public void added(Item item) {
initialize(item);
if (item instanceof GenericItem) {
GenericItem genericItem = (GenericItem) item;
genericItem.addStateChangeListener(this);
}
}
@Override
public void removed(Item item) {
if (item instanceof GenericItem) {
GenericItem genericItem = (GenericItem) item;
genericItem.removeStateChangeListener(this);
}
}
@Override
public void updated(Item oldItem, Item item) {
// not needed here
}
/*
* StateChangeListener
*/
@Override
public void stateChanged(Item item, State oldState, State newState) {
handleStateEvent(item, true);
}
@Override
public void stateUpdated(Item item, State state) {
handleStateEvent(item, false);
}
}