/* ==================================================================
* CASettingsService.java - Mar 12, 2012 1:11:29 PM
*
* Copyright 2007-2012 SolarNetwork.net Dev Team
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.settings.ca;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Dictionary;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import org.supercsv.cellprocessor.ConvertNullTo;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.io.CsvBeanReader;
import org.supercsv.io.CsvBeanWriter;
import org.supercsv.io.ICsvBeanReader;
import org.supercsv.io.ICsvBeanWriter;
import org.supercsv.prefs.CsvPreference;
import org.supercsv.util.CsvContext;
import net.solarnetwork.node.Setting;
import net.solarnetwork.node.Setting.SettingFlag;
import net.solarnetwork.node.SetupSettings;
import net.solarnetwork.node.backup.BackupResource;
import net.solarnetwork.node.backup.BackupResourceInfo;
import net.solarnetwork.node.backup.BackupResourceProvider;
import net.solarnetwork.node.backup.BackupResourceProviderInfo;
import net.solarnetwork.node.backup.ResourceBackupResource;
import net.solarnetwork.node.backup.SimpleBackupResourceInfo;
import net.solarnetwork.node.backup.SimpleBackupResourceProviderInfo;
import net.solarnetwork.node.dao.BasicBatchOptions;
import net.solarnetwork.node.dao.BatchableDao.BatchCallback;
import net.solarnetwork.node.dao.BatchableDao.BatchCallbackResult;
import net.solarnetwork.node.dao.SettingDao;
import net.solarnetwork.node.settings.FactorySettingSpecifierProvider;
import net.solarnetwork.node.settings.KeyedSettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.SettingSpecifierProviderFactory;
import net.solarnetwork.node.settings.SettingValueBean;
import net.solarnetwork.node.settings.SettingsBackup;
import net.solarnetwork.node.settings.SettingsCommand;
import net.solarnetwork.node.settings.SettingsImportOptions;
import net.solarnetwork.node.settings.SettingsService;
import net.solarnetwork.node.settings.support.BasicFactorySettingSpecifierProvider;
import net.solarnetwork.node.support.KeyValuePair;
/**
* Implementation of {@link SettingsService} that uses
* {@link ConfigurationAdmin} to change settings at runtime, and
* {@link SettingDao} to persist changes between application restarts.
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>configurationAdmin</dt>
* <dd>The {@link ConfigurationAdmin} service to use.</dd>
* </dl>
*
* @author matt
* @version 1.4
*/
public class CASettingsService implements SettingsService, BackupResourceProvider {
/** The OSGi service property key for the setting PID. */
public static final String OSGI_PROPERTY_KEY_SETTING_PID = "settingPid";
private static final String OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY = CASettingsService.class
.getName() + ".FACTORY_INSTANCE_KEY";
private static final String SETTING_LAST_BACKUP_DATE = "solarnode.settings.lastBackupDate";
private static final String BACKUP_DATE_FORMAT = "yyyy-MM-dd-HHmmss";
private static final String BACKUP_FILENAME_PREFIX = "settings_";
private static final String BACKUP_FILENAME_EXT = "txt";
private static final Pattern BACKUP_FILENAME_PATTERN = Pattern.compile('^' + BACKUP_FILENAME_PREFIX
+ "(\\d{4}-\\d{2}-\\d{2}-\\d{6})\\." + BACKUP_FILENAME_EXT + "$");
private static final int DEFAULT_BACKUP_MAX_COUNT = 5;
private static final String FACTORY_SETTING_KEY_SUFFIX = ".FACTORY";
// a CA PID pattern so that only these are attempted to be restored
private static final Pattern CA_PID_PATTERN = Pattern.compile("^[a-zA-Z0-9.]+$");
private ConfigurationAdmin configurationAdmin;
private SettingDao settingDao;
private TransactionTemplate transactionTemplate;
private String backupDestinationPath;
private int backupMaxCount = DEFAULT_BACKUP_MAX_COUNT;
private MessageSource messageSource;
private final Map<String, FactoryHelper> factories = new TreeMap<String, FactoryHelper>();
// private final Map<String, SettingSpecifierProviderFactory> factories =
// new TreeMap<String, SettingSpecifierProviderFactory>();
// private final Map<String, List<SettingSpecifierProvider>>
// factoryProviders = new HashMap<String, List<SettingSpecifierProvider>>();
private final Map<String, SettingSpecifierProvider> providers = new TreeMap<String, SettingSpecifierProvider>();
private final Logger log = LoggerFactory.getLogger(getClass());
private String getFactorySettingKey(String factoryPid) {
return factoryPid + FACTORY_SETTING_KEY_SUFFIX;
}
private String getFactoryInstanceSettingKey(String factoryPid, String instanceKey) {
return factoryPid + (instanceKey == null ? "" : "." + instanceKey);
}
/**
* Callback when a {@link SettingSpecifierProviderFactory} has been
* registered.
*
* @param provider
* the provider object
* @param properties
* the service properties
*/
public void onBindFactory(SettingSpecifierProviderFactory provider, Map<String, ?> properties) {
log.debug("Bind called on factory {} with props {}", provider, properties);
final String factoryPid = provider.getFactoryUID();
synchronized ( factories ) {
factories.put(factoryPid, new FactoryHelper(provider));
// find all configured factory instances, and publish those
// configurations now. First we look up all registered factory
// instances, so each returned result returns a configured instance
// key
List<KeyValuePair> instanceKeys = settingDao.getSettings(getFactorySettingKey(factoryPid));
for ( KeyValuePair instanceKey : instanceKeys ) {
SettingsCommand cmd = new SettingsCommand();
cmd.setProviderKey(factoryPid);
cmd.setInstanceKey(instanceKey.getKey());
// now lookup all settings for the configured instance
List<KeyValuePair> settings = settingDao
.getSettings(getFactoryInstanceSettingKey(factoryPid, instanceKey.getKey()));
for ( KeyValuePair setting : settings ) {
SettingValueBean bean = new SettingValueBean();
bean.setKey(setting.getKey());
bean.setValue(setting.getValue());
cmd.getValues().add(bean);
}
updateSettings(cmd);
}
}
}
/**
* Callback when a {@link SettingSpecifierProviderFactory} has been
* un-registered.
*
* @param config
* the configuration object
* @param properties
* the service properties
*/
public void onUnbindFactory(SettingSpecifierProviderFactory provider, Map<String, ?> properties) {
if ( provider == null ) {
// gemini blueprint calls this when availability="optional" and there are no services
return;
}
log.debug("Unbind called on factory {} with props {}", provider, properties);
final String pid = provider.getFactoryUID();
synchronized ( factories ) {
factories.remove(pid);
}
}
/**
* Callback when a {@link SettingSpecifierProvider} has been registered.
*
* @param provider
* the provider object
* @param properties
* the service properties
*/
public void onBind(SettingSpecifierProvider provider, Map<String, ?> properties) {
log.debug("Bind called on {} with props {}", provider, properties);
final String pid = provider.getSettingUID();
List<SettingSpecifierProvider> factoryList = null;
String factoryInstanceKey = null;
synchronized ( factories ) {
FactoryHelper helper = factories.get(pid);
if ( helper != null ) {
// Note: SERVICE_PID not normally provided by Spring: requires
// custom SN implementation bundle
String instancePid = (String) properties.get(Constants.SERVICE_PID);
Configuration conf;
try {
conf = configurationAdmin.getConfiguration(instancePid, null);
@SuppressWarnings("unchecked")
Dictionary<String, ?> props = conf.getProperties();
if ( props != null ) {
factoryInstanceKey = (String) props.get(OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY);
log.debug("Got factory {} instance key {}", pid, factoryInstanceKey);
factoryList = helper.getInstanceProviders(factoryInstanceKey);
factoryList.add(provider);
}
} catch ( IOException e ) {
log.error("Error getting factory instance configuration {}", instancePid, e);
}
}
}
if ( factoryList == null ) {
synchronized ( providers ) {
providers.put(pid, provider);
}
}
final String settingKey = getFactoryInstanceSettingKey(pid, factoryInstanceKey);
List<KeyValuePair> settings = settingDao.getSettings(settingKey);
if ( settings.size() < 1 ) {
return;
}
SettingsCommand cmd = new SettingsCommand();
for ( KeyValuePair pair : settings ) {
SettingValueBean bean = new SettingValueBean();
bean.setProviderKey(provider.getSettingUID());
bean.setInstanceKey(factoryInstanceKey);
bean.setKey(pair.getKey());
bean.setValue(pair.getValue());
cmd.getValues().add(bean);
}
updateSettings(cmd);
}
/**
* Callback when a {@link SettingSpecifierProvider} has been un-registered.
*
* @param config
* the configuration object
* @param properties
* the service properties
*/
public void onUnbind(SettingSpecifierProvider provider, Map<String, ?> properties) {
if ( provider == null ) {
// gemini blueprint calls this when availability="optional" and there are no services
return;
}
log.debug("Unbind called on {} with props {}", provider, properties);
final String pid = provider.getSettingUID();
synchronized ( factories ) {
FactoryHelper helper = factories.get(pid);
if ( helper != null ) {
helper.removeProvider(provider);
return;
}
}
synchronized ( providers ) {
providers.remove(pid);
}
}
@Override
public List<SettingSpecifierProvider> getProviders() {
synchronized ( providers ) {
return new ArrayList<SettingSpecifierProvider>(providers.values());
}
}
@Override
public List<SettingSpecifierProviderFactory> getProviderFactories() {
List<SettingSpecifierProviderFactory> results;
synchronized ( factories ) {
results = new ArrayList<SettingSpecifierProviderFactory>(factories.size());
for ( FactoryHelper helper : factories.values() ) {
results.add(helper.getFactory());
}
return results;
}
}
@Override
public SettingSpecifierProviderFactory getProviderFactory(String factoryUID) {
synchronized ( factories ) {
FactoryHelper helper = factories.get(factoryUID);
if ( helper != null ) {
return helper.getFactory();
}
return null;
}
}
@Override
public Map<String, List<FactorySettingSpecifierProvider>> getProvidersForFactory(String factoryUID) {
Map<String, List<FactorySettingSpecifierProvider>> results = new LinkedHashMap<String, List<FactorySettingSpecifierProvider>>();
synchronized ( factories ) {
FactoryHelper helper = factories.get(factoryUID);
if ( helper != null ) {
for ( Map.Entry<String, List<SettingSpecifierProvider>> me : helper
.instanceEntrySet() ) {
String instanceUID = me.getKey();
List<FactorySettingSpecifierProvider> list = new ArrayList<FactorySettingSpecifierProvider>(
me.getValue().size());
for ( SettingSpecifierProvider provider : me.getValue() ) {
list.add(new BasicFactorySettingSpecifierProvider(instanceUID, provider));
}
results.put(instanceUID, list);
}
}
}
return results;
}
@Override
public Object getSettingValue(SettingSpecifierProvider provider, SettingSpecifier setting) {
if ( setting instanceof KeyedSettingSpecifier<?> ) {
KeyedSettingSpecifier<?> keyedSetting = (KeyedSettingSpecifier<?>) setting;
if ( keyedSetting.isTransient() ) {
return keyedSetting.getDefaultValue();
}
final String providerUID = provider.getSettingUID();
final String instanceUID = (provider instanceof FactorySettingSpecifierProvider
? ((FactorySettingSpecifierProvider) provider).getFactoryInstanceUID() : null);
try {
Configuration conf = getConfiguration(providerUID, instanceUID);
@SuppressWarnings("unchecked")
Dictionary<String, ?> props = conf.getProperties();
Object val = (props == null ? null : props.get(keyedSetting.getKey()));
if ( val == null ) {
val = keyedSetting.getDefaultValue();
}
return val;
} catch ( IOException e ) {
throw new RuntimeException(e);
} catch ( InvalidSyntaxException e ) {
throw new RuntimeException(e);
}
}
return null;
}
private static final Pattern INDEXED_PROP_PATTERN = Pattern.compile("\\[\\d+\\]");
@SuppressWarnings("unchecked")
@Override
public void updateSettings(SettingsCommand command) {
// group all updates by provider+instance, to reduce the number of CA updates
// when multiple settings are changed
if ( command.getProviderKey() == null ) {
Map<String, SettingsCommand> groups = new LinkedHashMap<String, SettingsCommand>(8);
Map<String, SettingsCommand> indexedGroups = null;
for ( SettingValueBean bean : command.getValues() ) {
String groupKey = bean.getProviderKey()
+ (bean.getInstanceKey() == null ? "" : bean.getInstanceKey());
final boolean indexed = INDEXED_PROP_PATTERN.matcher(bean.getKey()).find();
SettingsCommand cmd = null;
if ( indexed ) {
// indexed property, add in indexed groups
if ( indexedGroups == null ) {
indexedGroups = new LinkedHashMap<String, SettingsCommand>(8);
}
cmd = indexedGroups.get(groupKey);
} else {
cmd = groups.get(groupKey);
}
if ( cmd == null ) {
cmd = new SettingsCommand();
cmd.setProviderKey(bean.getProviderKey());
cmd.setInstanceKey(bean.getInstanceKey());
if ( indexed ) {
indexedGroups.put(groupKey, cmd);
} else {
groups.put(groupKey, cmd);
}
}
cmd.getValues().add(bean);
}
for ( SettingsCommand cmd : groups.values() ) {
updateSettings(cmd);
}
if ( indexedGroups != null ) {
for ( SettingsCommand cmd : indexedGroups.values() ) {
updateSettings(cmd);
}
}
return;
}
try {
Configuration conf = getConfiguration(command.getProviderKey(), command.getInstanceKey());
Dictionary<String, Object> props = conf.getProperties();
if ( props == null ) {
props = new Hashtable<String, Object>();
}
for ( SettingValueBean bean : command.getValues() ) {
String settingKey = command.getProviderKey();
String instanceKey = command.getInstanceKey();
if ( instanceKey != null ) {
settingKey = getFactoryInstanceSettingKey(settingKey, instanceKey);
}
if ( bean.isRemove() ) {
props.remove(bean.getKey());
} else {
props.put(bean.getKey(), bean.getValue());
}
if ( !bean.isTransient() ) {
if ( bean.isRemove() ) {
settingDao.deleteSetting(settingKey, bean.getKey());
} else {
settingDao.storeSetting(settingKey, bean.getKey(), bean.getValue());
}
}
}
if ( conf != null && props != null ) {
if ( command.getInstanceKey() != null ) {
props.put(OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY, command.getInstanceKey());
}
conf.update(props);
}
} catch ( IOException e ) {
throw new RuntimeException(e);
} catch ( InvalidSyntaxException e ) {
throw new RuntimeException(e);
}
}
@Override
public String addProviderFactoryInstance(String factoryUID) {
synchronized ( factories ) {
List<KeyValuePair> instanceKeys = settingDao.getSettings(getFactorySettingKey(factoryUID));
int next = instanceKeys.size() + 1;
// verify key doesn't exist
boolean done = false;
while ( !done ) {
done = true;
for ( KeyValuePair instanceKey : instanceKeys ) {
if ( instanceKey.getKey().equals(String.valueOf(next)) ) {
done = false;
next++;
}
}
}
String newInstanceKey = String.valueOf(next);
settingDao.storeSetting(getFactorySettingKey(factoryUID), newInstanceKey, newInstanceKey);
try {
Configuration conf = getConfiguration(factoryUID, newInstanceKey);
@SuppressWarnings("unchecked")
Dictionary<String, Object> props = conf.getProperties();
if ( props == null ) {
props = new Hashtable<String, Object>();
}
props.put(OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY, newInstanceKey);
conf.update(props);
return newInstanceKey;
} catch ( IOException e ) {
throw new RuntimeException(e);
} catch ( InvalidSyntaxException e ) {
throw new RuntimeException(e);
}
}
}
@Override
public void deleteProviderFactoryInstance(String factoryUID, String instanceUID) {
synchronized ( factories ) {
// delete factory reference
settingDao.deleteSetting(getFactorySettingKey(factoryUID), instanceUID);
// delete instance values
settingDao.deleteSetting(getFactoryInstanceSettingKey(factoryUID, instanceUID));
// delete Configuration
try {
Configuration conf = getConfiguration(factoryUID, instanceUID);
conf.delete();
} catch ( IOException e ) {
throw new RuntimeException(e);
} catch ( InvalidSyntaxException e ) {
throw new RuntimeException(e);
}
}
}
private static final String[] CSV_HEADERS = new String[] { "key", "type", "value", "flags",
"modified" };
private static final String SETTING_MODIFIED_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Override
public void exportSettingsCSV(Writer out) throws IOException {
final ICsvBeanWriter writer = new CsvBeanWriter(out, CsvPreference.STANDARD_PREFERENCE);
final List<IOException> errors = new ArrayList<IOException>(1);
final CellProcessor[] processors = new CellProcessor[] {
new org.supercsv.cellprocessor.Optional(), new org.supercsv.cellprocessor.Optional(),
new org.supercsv.cellprocessor.Optional(), new CellProcessor() {
@Override
public Object execute(Object value, CsvContext ctx) {
@SuppressWarnings("unchecked")
Set<net.solarnetwork.node.Setting.SettingFlag> set = (Set<net.solarnetwork.node.Setting.SettingFlag>) value;
if ( set != null ) {
return net.solarnetwork.node.Setting.SettingFlag.maskForSet(set);
}
return 0;
}
}, new org.supercsv.cellprocessor.FmtDate(SETTING_MODIFIED_DATE_FORMAT) };
try {
writer.writeHeader(CSV_HEADERS);
settingDao.batchProcess(new BatchCallback<Setting>() {
@Override
public BatchCallbackResult handle(Setting domainObject) {
try {
writer.write(domainObject, CSV_HEADERS, processors);
} catch ( IOException e ) {
errors.add(e);
return BatchCallbackResult.STOP;
}
return BatchCallbackResult.CONTINUE;
}
}, new BasicBatchOptions("Export Settings"));
if ( errors.size() > 0 ) {
throw errors.get(0);
}
} finally {
if ( writer != null ) {
try {
writer.flush();
writer.close();
} catch ( IOException e ) {
// ignore these
}
}
}
}
/**
* A callback API for allowing the settings import process to decide which
* settings should be imported.
*/
private interface ImportCallback {
/**
* Test if a specific should be imported at all.
*
* @param key
* the setting key
* @param type
* the setting value
* @param value
* the setting value
* @return <em>true</em> to allow the setting to be imported,
* <em>false</em> to skip
*/
boolean shouldImportSetting(Setting setting);
}
@Override
public void importSettingsCSV(Reader in) throws IOException {
importSettingsCSV(in, new SettingsImportOptions());
}
@Override
public void importSettingsCSV(final Reader in, final SettingsImportOptions options)
throws IOException {
// TODO: need a better way to organize settings into "do not restore" category
synchronized ( factories ) {
final Pattern allowed = Pattern.compile("^(?!solarnode).*", Pattern.CASE_INSENSITIVE);
importSettingsCSV(in, new ImportCallback() {
@Override
public boolean shouldImportSetting(Setting s) {
if ( allowed.matcher(s.getKey()).matches() == false ) {
return false;
}
if ( options.isAddOnly() ) {
// check if setting exists already, and if so do not import it
if ( settingDao.getSetting(s.getKey(), s.getType()) != null ) {
log.debug("Not updating existing setting {}", s.getKey());
return false;
}
}
return true;
}
});
}
}
private void importSettingsCSV(Reader in, final ImportCallback callback) throws IOException {
final ICsvBeanReader reader = new CsvBeanReader(in, CsvPreference.STANDARD_PREFERENCE);
final CellProcessor[] processors = new CellProcessor[] { null, new ConvertNullTo(""), null,
new CellProcessor() {
@Override
public Object execute(Object arg, CsvContext ctx) {
Set<net.solarnetwork.node.Setting.SettingFlag> set = null;
if ( arg != null ) {
int mask = Integer.parseInt(arg.toString());
set = net.solarnetwork.node.Setting.SettingFlag.setForMask(mask);
}
return set;
}
}, new org.supercsv.cellprocessor.ParseDate(SETTING_MODIFIED_DATE_FORMAT) };
reader.getHeader(true);
final List<Setting> importedSettings = new ArrayList<Setting>();
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
Setting s;
try {
while ( (s = reader.read(Setting.class, CSV_HEADERS, processors)) != null ) {
if ( !callback.shouldImportSetting(s) ) {
continue;
}
if ( s.getKey() == null ) {
continue;
}
if ( s.getValue() == null ) {
settingDao.deleteSetting(s.getKey(), s.getType());
} else {
settingDao.storeSetting(s);
importedSettings.add(s);
}
}
} catch ( IOException e ) {
log.error("Unable to import settings: {}", e.getMessage());
status.setRollbackOnly();
} finally {
try {
reader.close();
} catch ( IOException e ) {
// ingore
}
if ( status.isRollbackOnly() ) {
importedSettings.clear();
}
}
}
});
// now that settings have been imported into DAO layer, we need to apply them to the existing runtime
// first, determine what factories we have... these have keys like <factoryPID>.FACTORY
final Map<String, Setting> factorySettings = new HashMap<String, Setting>();
for ( Setting s : importedSettings ) {
if ( s.getKey() == null || !s.getKey().endsWith(FACTORY_SETTING_KEY_SUFFIX) ) {
continue;
}
String factoryPID = s.getKey().substring(0,
s.getKey().length() - FACTORY_SETTING_KEY_SUFFIX.length());
log.debug("Discovered imported factory setting {}", factoryPID);
factorySettings.put(factoryPID, s);
// Now create the CA configuration for all defined factories, to handle situation where we don't actually
// configure any custom settings on the factory. In that case we don't have any settings, but we need
// to instantiate the factory so we create a default instance.
try {
int instanceCount = Integer.valueOf(s.getValue());
for ( int i = 1; i <= instanceCount; i++ ) {
String instanceKey = String.valueOf(i);
Configuration conf = getConfiguration(factoryPID, instanceKey);
@SuppressWarnings("unchecked")
Dictionary<String, Object> props = conf.getProperties();
if ( props == null ) {
props = new Hashtable<String, Object>();
props.put(OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY, instanceKey);
conf.update(props);
}
}
} catch ( NumberFormatException e ) {
log.warn("Factory {} setting does not have instance count value: {}", factoryPID,
e.getMessage());
} catch ( InvalidSyntaxException e ) {
log.warn("Factory {} setting has invalid syntax: {}", factoryPID, e.getMessage());
}
}
// now convert imported settings into a SettingsCommand, so values are applied to Configuration Admin
SettingsCommand cmd = new SettingsCommand();
for ( Setting s : importedSettings ) {
if ( s.getKey() == null ) {
continue;
}
// skip factory instance definitions
if ( s.getKey().endsWith(FACTORY_SETTING_KEY_SUFFIX) ) {
continue;
}
// skip things that don't look like CA settings
if ( !CA_PID_PATTERN.matcher(s.getKey()).matches() || s.getType() == null
|| SetupSettings.SETUP_TYPE_KEY.equals(s.getType()) || s.getType().length() < 1 ) {
continue;
}
SettingValueBean bean = new SettingValueBean();
// find out if this is a factory
for ( String factoryPID : factorySettings.keySet() ) {
if ( s.getKey().startsWith(factoryPID + ".")
&& s.getKey().length() > (factoryPID.length() + 1) ) {
bean.setProviderKey(factoryPID);
bean.setInstanceKey(s.getKey().substring(factoryPID.length() + 1));
break;
}
}
if ( bean.getProviderKey() == null ) {
// not a factory setting
bean.setProviderKey(s.getKey());
}
bean.setKey(s.getType());
bean.setValue(s.getValue());
bean.setTransient(s.getFlags() != null && s.getFlags().contains(SettingFlag.Volatile));
cmd.getValues().add(bean);
}
if ( cmd.getValues().size() > 0 ) {
updateSettings(cmd);
}
}
@Override
public SettingsBackup backupSettings() {
final Date mrd = settingDao.getMostRecentModificationDate();
final SimpleDateFormat sdf = new SimpleDateFormat(BACKUP_DATE_FORMAT);
final String lastBackupDateStr = settingDao.getSetting(SETTING_LAST_BACKUP_DATE);
final Date lastBackupDate;
try {
lastBackupDate = (lastBackupDateStr == null ? null : sdf.parse(lastBackupDateStr));
} catch ( ParseException e ) {
throw new RuntimeException("Unable to parse backup last date: " + e.getMessage());
}
if ( mrd == null || (lastBackupDate != null && lastBackupDate.after(mrd)) ) {
log.debug("Settings unchanged since last backup on {}", lastBackupDateStr);
return null;
}
final Date backupDate = new Date();
final String backupDateKey = sdf.format(backupDate);
final File dir = new File(backupDestinationPath);
if ( !dir.exists() ) {
dir.mkdirs();
}
final File f = new File(dir, BACKUP_FILENAME_PREFIX + backupDateKey + '.' + BACKUP_FILENAME_EXT);
log.info("Backing up settings to {}", f.getPath());
Writer writer = null;
try {
writer = new BufferedWriter(new FileWriter(f));
exportSettingsCSV(writer);
settingDao.storeSetting(new Setting(SETTING_LAST_BACKUP_DATE, null, backupDateKey,
EnumSet.of(SettingFlag.IgnoreModificationDate)));
} catch ( IOException e ) {
log.error("Unable to create settings backup {}: {}", f.getPath(), e.getMessage());
} finally {
try {
writer.flush();
writer.close();
} catch ( IOException e ) {
// ignore
}
}
// clean out older backups
File[] files = dir.listFiles(new RegexFileFilter(BACKUP_FILENAME_PATTERN));
if ( files != null && files.length > backupMaxCount ) {
// sort array
Arrays.sort(files, new FilenameReverseComparator());
for ( int i = backupMaxCount; i < files.length; i++ ) {
if ( !files[i].delete() ) {
log.warn("Unable to delete old settings backup file {}", files[i]);
}
}
}
return new SettingsBackup(backupDateKey, backupDate);
}
@Override
public Collection<SettingsBackup> getAvailableBackups() {
final File dir = new File(backupDestinationPath);
File[] files = dir.listFiles(new RegexFileFilter(BACKUP_FILENAME_PATTERN));
if ( files == null || files.length == 0 ) {
return Collections.emptyList();
}
Arrays.sort(files, new FilenameReverseComparator());
List<SettingsBackup> list = new ArrayList<SettingsBackup>(files.length);
SimpleDateFormat sdf = new SimpleDateFormat(BACKUP_DATE_FORMAT);
for ( File f : files ) {
Matcher m = BACKUP_FILENAME_PATTERN.matcher(f.getName());
if ( m.matches() ) {
String dateStr = m.group(1);
try {
list.add(new SettingsBackup(dateStr, sdf.parse(dateStr)));
} catch ( ParseException e ) {
log.warn("Unable to parse backup file date from filename {}: {}", f.getName(),
e.getMessage());
}
}
}
return list;
}
@Override
public Reader getReaderForBackup(SettingsBackup backup) {
final File dir = new File(backupDestinationPath);
try {
final String fname = BACKUP_FILENAME_PREFIX + backup.getBackupKey() + '.'
+ BACKUP_FILENAME_EXT;
final File f = new File(dir, fname);
if ( f.canRead() ) {
return new BufferedReader(new FileReader(f));
}
} catch ( FileNotFoundException e ) {
return null;
}
return null;
}
@Override
public String getKey() {
return CASettingsService.class.getName();
}
private static final String BACKUP_RESOURCE_SETTINGS_CSV = "settings.csv";
@Override
public Iterable<BackupResource> getBackupResources() {
// create resource from our settings CSV data
ByteArrayOutputStream byos = new ByteArrayOutputStream();
try {
OutputStreamWriter writer = new OutputStreamWriter(byos, "UTF-8");
exportSettingsCSV(writer);
} catch ( IOException e ) {
log.error("Unable to create settings backup resource", e);
}
List<BackupResource> resources = new ArrayList<BackupResource>(1);
resources.add(new ResourceBackupResource(new ByteArrayResource(byos.toByteArray()),
BACKUP_RESOURCE_SETTINGS_CSV, getKey()));
return resources;
}
@Override
public boolean restoreBackupResource(BackupResource resource) {
if ( BACKUP_RESOURCE_SETTINGS_CSV.equalsIgnoreCase(resource.getBackupPath()) ) {
try {
// TODO: need a better way to organize settings into "do not restore" category
final Pattern notAllowed = Pattern.compile("^solarnode.*", Pattern.CASE_INSENSITIVE);
InputStreamReader reader = new InputStreamReader(resource.getInputStream(), "UTF-8");
importSettingsCSV(reader, new ImportCallback() {
@Override
public boolean shouldImportSetting(Setting s) {
if ( notAllowed.matcher(s.getKey()).matches() ) {
// only allow restoring solarnode keys if their type is NOT empty
return (s.getType() != null && s.getType().length() > 0);
}
return true;
}
});
return true;
} catch ( IOException e ) {
log.error("Unable to restore settings backup resource", e);
}
}
return false;
}
@Override
public BackupResourceProviderInfo providerInfo(Locale locale) {
String name = "Settings Backup Provider";
String desc = "Backs up system settings.";
MessageSource ms = messageSource;
if ( ms != null ) {
name = ms.getMessage("title", null, name, locale);
desc = ms.getMessage("desc", null, desc, locale);
}
return new SimpleBackupResourceProviderInfo(getKey(), name, desc);
}
@Override
public BackupResourceInfo resourceInfo(BackupResource resource, Locale locale) {
return new SimpleBackupResourceInfo(resource.getProviderKey(), resource.getBackupPath(), null);
}
private static class FilenameReverseComparator implements Comparator<File> {
@Override
public int compare(File o1, File o2) {
// order in reverse, and then we can delete all but maxBackupCount
return o2.getName().compareTo(o1.getName());
}
}
private static class RegexFileFilter implements FileFilter {
final Pattern p;
private RegexFileFilter(Pattern p) {
super();
this.p = p;
}
@Override
public boolean accept(File pathname) {
Matcher m = p.matcher(pathname.getName());
return m.matches();
}
}
private Configuration getConfiguration(String providerUID, String factoryInstanceUID)
throws IOException, InvalidSyntaxException {
Configuration conf = null;
if ( factoryInstanceUID == null ) {
conf = configurationAdmin.getConfiguration(providerUID, null);
} else {
conf = findExistingConfiguration(providerUID, factoryInstanceUID);
if ( conf == null ) {
conf = configurationAdmin.createFactoryConfiguration(providerUID, null);
}
}
return conf;
}
private Configuration findExistingConfiguration(String pid, String instanceKey)
throws IOException, InvalidSyntaxException {
String filter = "(&(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + pid + ")("
+ OSGI_PROPERTY_KEY_FACTORY_INSTANCE_KEY + "=" + instanceKey + "))";
Configuration[] configurations = configurationAdmin.listConfigurations(filter);
if ( configurations != null && configurations.length > 0 ) {
return configurations[0];
} else {
return null;
}
}
public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
this.configurationAdmin = configurationAdmin;
}
public void setSettingDao(SettingDao settingDao) {
this.settingDao = settingDao;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void setBackupDestinationPath(String backupDestinationPath) {
this.backupDestinationPath = backupDestinationPath;
}
public void setBackupMaxCount(int backupMaxCount) {
this.backupMaxCount = backupMaxCount;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}