/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.settings;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nl.strohalm.cyclos.dao.settings.SettingDAO;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.settings.Setting;
import nl.strohalm.cyclos.entities.settings.events.SettingsChangeListener;
import nl.strohalm.cyclos.utils.ClassHelper;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.cache.Cache;
import nl.strohalm.cyclos.utils.cache.CacheAdapter;
import nl.strohalm.cyclos.utils.cache.CacheCallback;
import nl.strohalm.cyclos.utils.cache.CacheListener;
import nl.strohalm.cyclos.utils.cache.CacheManager;
import nl.strohalm.cyclos.utils.conversion.Converter;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionRollbackListener;
import nl.strohalm.cyclos.utils.validation.Validator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
/**
* Base implementation for settings handler
* @author luis
*/
public abstract class BaseSettingsHandler<T, L extends SettingsChangeListener> implements SettingsHandler<T, L>, InitializingBean {
private final Log LOGGER = LogFactory.getLog(getClass());
private final Class<T> beanClass;
private final Validator validator;
private CacheManager cacheManager;
private SettingDAO settingDao;
private final Set<L> listeners = new HashSet<L>();
final Setting.Type type;
final Map<String, Converter<?>> converters;
protected BaseSettingsHandler(final Setting.Type type, final Class<T> beanClass) {
this.type = type;
this.beanClass = beanClass;
this.converters = Collections.unmodifiableMap(createConverters());
this.validator = createValidator();
}
@Override
public void addListener(final L listener) {
listeners.add(listener);
}
@Override
public void afterPropertiesSet() throws Exception {
CacheListener listener = new CacheAdapter() {
@Override
@SuppressWarnings("unchecked")
public void onValueAdded(final Cache cache, final Serializable key, final Object value) {
// As all cache beans are stored on the same cache, let's see if this one is of the type handled by this handler
if (!beanClass.isInstance(value)) {
return;
}
T settings = (T) value;
for (L listener : listeners) {
notifyListener(listener, settings);
}
}
};
getCache().addListener(listener);
}
/**
* Returns the settings bean
*/
@Override
public T get() {
return getCache().<T> get(getCacheKey(), new CacheCallback() {
@Override
public Object retrieve() {
return read();
}
});
}
/**
* Returns the settings bean class
*/
public Class<T> getBeanClass() {
return beanClass;
}
public SettingDAO getSettingDao() {
return settingDao;
}
@Override
public T importFrom(final Map<String, String> values) {
final T object = ClassHelper.instantiate(beanClass);
populate(object, values);
return update(object);
}
@Override
public List<Setting> listSettings() {
return buildSettings(get());
}
@Override
public void refresh() {
getCache().remove(getCacheKey());
}
@Override
public void removeListener(final L listener) {
listeners.remove(listener);
}
public void setCacheManager(final CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setSettingDao(final SettingDAO settingsDao) {
this.settingDao = settingsDao;
}
/**
* Updates the settings bean
*/
@Override
public T update(final T newSettings) {
// If the transaction is rolled back, the cache must be refreshed to restore the old values.
CurrentTransactionData.addTransactionRollbackListener(new TransactionRollbackListener() {
@Override
public void onTransactionRollback() {
refresh();
}
});
// Validate the bean
validate(newSettings);
// Build a list of Setting entities
final List<Setting> list = buildSettings(newSettings);
for (final Setting setting : list) {
Setting loaded = null;
try {
loaded = settingDao.load(type, setting.getName());
// The setting exists: update the value
loaded.setValue(setting.getValue());
settingDao.update(loaded);
} catch (final EntityNotFoundException e) {
// The setting didn't exists
settingDao.insert(setting);
}
}
// Re-read the cached instance
refresh();
return get();
}
/**
* Validate the settings bean
*/
@Override
public void validate(final T settings) {
validator.validate(settings);
}
/**
* Must be overriden to create the converters
*/
protected abstract Map<String, Converter<?>> createConverters();
/**
* Must be overriden to create the validator
*/
protected abstract Validator createValidator();
/**
* Should be implemented in order to notify the given listener about a setting change
*/
protected abstract void notifyListener(L listener, T settings);
/**
* Read a settings bean from a list of Setting objects
*/
protected T read() {
final List<Setting> settingsList = settingDao.listByType(type);
final Map<String, String> values = new HashMap<String, String>();
for (final Setting setting : settingsList) {
values.put(setting.getName(), setting.getValue());
}
try {
final T settings = getBeanClass().newInstance();
populate(settings, values);
return settings;
} catch (final Exception e) {
LOGGER.warn("Error creating a settings bean", e);
throw new IllegalStateException(e);
}
}
@SuppressWarnings("unchecked")
private List<Setting> buildSettings(final T settings) {
final List<Setting> values = new LinkedList<Setting>();
for (final Map.Entry<String, Converter<?>> entry : converters.entrySet()) {
final String name = entry.getKey();
// Read the value from the settings bean
final Object value = PropertyHelper.get(settings, name);
final String valueAsString = PropertyHelper.getAsString(value, (Converter<Object>) entry.getValue());
// Create a setting entity
final Setting setting = new Setting();
setting.setType(type);
setting.setName(name);
setting.setValue(valueAsString);
values.add(setting);
}
return values;
}
private Cache getCache() {
return cacheManager.getCache("cyclos.Settings");
}
private String getCacheKey() {
return beanClass.getSimpleName();
}
/**
* Populate a settings object, using a Map of converters, and a Map of values. Only 2 levels of beans are supported, ie, xxxSettings.x.y. If there
* were a nested bean on x, making it be xxxSettings.x.y.z, z would be ignored
*/
private void populate(final Object settings, final Map<String, String> values) {
for (final Map.Entry<String, Converter<?>> entry : converters.entrySet()) {
final String name = entry.getKey();
final Converter<?> converter = entry.getValue();
if (values.containsKey(name)) {
final String value = values.get(name);
final Object realValue = converter.valueOf(value);
// Check if there is a nested object
if (name.contains(".")) {
final String first = PropertyHelper.firstProperty(name);
// No bean: instantiate it
if (PropertyHelper.get(settings, first) == null) {
try {
final Class<?> type = PropertyUtils.getPropertyType(settings, first);
final Object bean = type.newInstance();
PropertyHelper.set(settings, first, bean);
} catch (final Exception e) {
LOGGER.warn("Error while setting nested settings bean", e);
throw new IllegalStateException();
}
}
}
PropertyHelper.set(settings, name, realValue);
}
}
}
}