/**
   * 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 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);
  }
  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);
    }
  }
  @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);
    }
  }