com.vmware.appfactory.config.ConfigRegistry.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.appfactory.config.ConfigRegistry.java

Source

/* ***********************************************************************
 * VMware ThinApp Factory
 * Copyright (c) 2009-2013 VMware, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ***********************************************************************/

package com.vmware.appfactory.config;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.Resource;
import javax.annotation.concurrent.GuardedBy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;

import com.google.common.base.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.vmware.appfactory.common.dto.EachOption;
import com.vmware.appfactory.common.dto.GroupOption;
import com.vmware.appfactory.common.dto.SingleOption;
import com.vmware.appfactory.common.exceptions.AfNotFoundException;
import com.vmware.appfactory.config.dao.ConfigDao;
import com.vmware.appfactory.config.model.ConfigSetting;
import com.vmware.appfactory.datastore.DatastoreClientService;
import com.vmware.appfactory.datastore.DsDatastore;
import com.vmware.appfactory.workpool.WorkpoolClientService;
import com.vmware.thinapp.common.converter.client.ThinAppRuntimeClient;
import com.vmware.thinapp.common.converter.dto.ThinAppRuntime;
import com.vmware.thinapp.common.datastore.dto.Datastore;
import com.vmware.thinapp.common.workpool.dto.Workpool;
import com.vmware.thinapp.common.workpool.exception.WpException;

import static com.vmware.appfactory.config.ConfigParam.*;
import static com.vmware.appfactory.config.ConfigRegistryConstants.*;

/**
 * This class handles all configuration operations for AppFactory. It defines
 * all the available configuration parameters, and methods to set and get values
 * for each one.
 *
 * Defaults for parameters are defined in resources/config.properties. If no
 * default is defined there, the default value will be 0, false, or "" as
 * appropriate.
 */
@SuppressWarnings("MissortedModifiers")
public class ConfigRegistry {
    /**
     * Get the logger
     */
    private final Logger _log = LoggerFactory.getLogger(ConfigRegistry.class);

    /**
     * This is the set of default configuration parameters.
     */
    private static final Map<String, ConfigParam> _PARAM_MAP = init();

    @Resource
    private ConfigDao _configDao;

    @GuardedBy(value = "this")
    private Map<String, ConfigSetting> _settingsInDBMap;

    private LoadingCache<String, String> _stringCache;
    private LoadingCache<String, Boolean> _booleanCache;
    private LoadingCache<String, Integer> _intCache;
    private LoadingCache<String, Long> _longCache;

    /**
     * Initialize the config parameters map
     * @return a map of config parameters.
     */
    private static Map<String, ConfigParam> init() {
        final ImmutableMap.Builder<String, ConfigParam> builder = ImmutableMap.builder();
        int n = 0;
        /** Horizon **/
        builder.put(HORIZON_ENABLED, newBooleanInstance(CONF_GROUP_HORIZON, n++, HORIZON_ENABLED, false,
                "Enabling horizon integration prompts for configuring horizon."));
        builder.put(HORIZON_URL, newStringInstance(CONF_GROUP_HORIZON, n++, HORIZON_URL, false));
        builder.put(HORIZON_IGNORE_CERT_WARN,
                newBooleanInstance(CONF_GROUP_HORIZON, n++, HORIZON_IGNORE_CERT_WARN, false));
        builder.put(HORIZON_IDP_ACTIVATION_TOKEN,
                newStringInstance(CONF_GROUP_HORIZON, n++, HORIZON_IDP_ACTIVATION_TOKEN, false));
        builder.put(HORIZON_CLIENT_USERNAME,
                newStringInstance(CONF_GROUP_HORIZON, n++, HORIZON_CLIENT_USERNAME, false));
        builder.put(HORIZON_CLIENT_PASSWORD,
                newStringInstance(CONF_GROUP_HORIZON, n++, HORIZON_CLIENT_PASSWORD, false));
        builder.put(HORIZON_OAUTH2_TOKEN, newStringInstance(CONF_GROUP_HORIZON, n++, HORIZON_OAUTH2_TOKEN, false));

        /** General **/
        builder.put(GEN_REFRESH_PERIOD_SECS,
                newLongInstance(CONF_GROUP_GENERAL, n++, GEN_REFRESH_PERIOD_SECS, false, "seconds (0 to suspend)"));
        builder.put(GEN_SKIN, newSelectInstance(CONF_GROUP_GENERAL, n++, GEN_SKIN, false, new SkinValues()));
        builder.put(GEN_EULA_ACCEPTED, newBooleanInstance(CONF_GROUP_GENERAL, n++, GEN_EULA_ACCEPTED, false));

        /** Feeds **/
        builder.put(FEED_RETRY_FAILED_SCAN,
                newBooleanInstance(CONF_GROUP_FEEDS, n++, FEED_RETRY_FAILED_SCAN, true));
        builder.put(FEED_RETRY_FAILED_CONVERT,
                newBooleanInstance(CONF_GROUP_FEEDS, n++, FEED_RETRY_FAILED_CONVERT, true));
        builder.put(FEED_RESCAN_PERIOD_MINS,
                newLongInstance(CONF_GROUP_FEEDS, n++, FEED_RESCAN_PERIOD_MINS, true, "minutes"));
        builder.put(FEEDS_MAX_CONVERT_ATTEMPTS,
                newLongInstance(CONF_GROUP_FEEDS, n++, FEEDS_MAX_CONVERT_ATTEMPTS, false, "attempts"));

        /** File Share **/
        builder.put(FILESHARE_MAX_DIR_DEPTH_SCAN,
                newLongInstance(CONF_GROUP_FILESHARES, n++, FILESHARE_MAX_DIR_DEPTH_SCAN, true, "subdirectories"));
        builder.put(FILESHARE_DIR_LAYOUT,
                newSelectInstance(CONF_GROUP_FILESHARES, n++, FILESHARE_DIR_LAYOUT, true, new DirLayoutValues()));
        builder.put(FILESHARE_RECIPE_DIR,
                newStringInstance(CONF_GROUP_FILESHARES, n++, FILESHARE_RECIPE_DIR, false));
        builder.put(FILESHARE_OVERRIDE_APP_INFO_IN_RESCAN,
                newBooleanInstance(CONF_GROUP_FILESHARES, n++, FILESHARE_OVERRIDE_APP_INFO_IN_RESCAN, true));

        /** Tasks **/
        builder.put(TASKQ_MAX_PROJECTS_PER_BATCH,
                newIntegerInstance(CONF_GROUP_TASKS, n++, TASKQ_MAX_PROJECTS_PER_BATCH, true, "per batch request"));
        builder.put(TASKQ_MAX_CONCURRENT, newIntegerInstance(CONF_GROUP_TASKS, n++, TASKQ_MAX_CONCURRENT, true,
                "conversion tasks (-1 to auto-set equal to # of vm images)"));
        builder.put(TASKQ_MAX_FINISHED_COUNT, newIntegerInstance(CONF_GROUP_TASKS, n++, TASKQ_MAX_FINISHED_COUNT,
                true,
                "completed conversion tasks to keep (-1 for the max, limited to 1000.  must restart server to take effect)"));
        builder.put(TASKQ_MAX_CONCURRENT_SCANS,
                newIntegerInstance(CONF_GROUP_TASKS, n++, TASKQ_MAX_CONCURRENT_SCANS, true,
                        "feed scan tasks (-1 to auto-set to number of subscribed feeds)"));
        builder.put(TASKQ_MAX_FINISHED_SCANS, newIntegerInstance(CONF_GROUP_TASKS, n++, TASKQ_MAX_FINISHED_SCANS,
                true,
                "completed feed scan tasks to keep (-1 for the max, limited to 1000.  must restart server to take effect)"));

        /** CWS **/
        builder.put(CWS_SERVICE_URL, newStringInstance(CONF_GROUP_CWS, n++, CWS_SERVICE_URL, false));
        builder.put(CWS_CONVERSIONS_URL, newStringInstance(CONF_GROUP_CWS, n++, CWS_CONVERSIONS_URL, false));
        builder.put(CWS_PAUSED, newBooleanInstance(CONF_GROUP_CWS, n++, CWS_PAUSED, false));
        builder.put(THINAPP_RUNTIME_ID,
                newSelectInstance(CONF_GROUP_CWS, n++, THINAPP_RUNTIME_ID, true, new ConfigParamOptions() {
                    @Override
                    public List<EachOption> getValues(ConfigRegistry reg) {
                        return reg.getRuntimeOptions();
                    }
                }));
        builder.put(CWS_STALL_TIMEOUT, newLongInstance(CONF_GROUP_CWS, n++, CWS_STALL_TIMEOUT, true, "secs"));
        builder.put(CWS_STALL_CPU, newLongInstance(CONF_GROUP_CWS, n++, CWS_STALL_CPU, true, "%"));
        builder.put(CWS_STALL_NET, newLongInstance(CONF_GROUP_CWS, n++, CWS_STALL_NET, true, "%"));
        builder.put(CWS_STALL_DISK, newLongInstance(CONF_GROUP_CWS, n++, CWS_STALL_DISK, true, "%"));
        builder.put(CWS_ENABLE_QUALITY_REPORTING,
                newBooleanInstance(CONF_GROUP_CWS, n++, CWS_ENABLE_QUALITY_REPORTING, true));
        builder.put(CWS_QUALITY_REPORTING_TAG,
                newStringInstance(CONF_GROUP_CWS, n++, CWS_QUALITY_REPORTING_TAG, false));

        /** Datastore **/
        builder.put(DATASTORE_SERVICE_URL,
                newStringInstance(CONF_GROUP_STORAGE, n++, DATASTORE_SERVICE_URL, false));
        builder.put(DATASTORE_DEFAULT_OUTPUT_ID, newSelectInstance(CONF_GROUP_STORAGE, n++,
                DATASTORE_DEFAULT_OUTPUT_ID, true, new DefaultDatastoreValues()));
        builder.put(DATASTORE_DEFAULT_RECIPE_ID, newSelectInstance(CONF_GROUP_STORAGE, n++,
                DATASTORE_DEFAULT_RECIPE_ID, true, new DefaultDatastoreValues()));
        builder.put(DATASTORE_SHOW_OFFLINE,
                newBooleanInstance(CONF_GROUP_STORAGE, n++, DATASTORE_SHOW_OFFLINE, true));

        /** Workpool **/
        builder.put(WORKPOOL_SERVICE_URL, newStringInstance(CONF_GROUP_WORKPOOL, n++, WORKPOOL_SERVICE_URL, false));
        builder.put(WORKPOOL_DEFAULT_WORKPOOL, newSelectInstance(CONF_GROUP_WORKPOOL, n++,
                WORKPOOL_DEFAULT_WORKPOOL, false, new WorkpoolValues()));
        builder.put(WORKPOOL_VI_TYPE,
                newSelectInstance(CONF_GROUP_WORKPOOL, n++, WORKPOOL_VI_TYPE, false, new viTypes()));
        builder.put(WORKPOOL_DEFAULT_MAX_INSTANCE, newIntegerInstance(CONF_GROUP_WORKPOOL, n++,
                WORKPOOL_DEFAULT_MAX_INSTANCE, false, "(applicable when deployed to vCenter)"));
        builder.put(WORKPOOL_SHOW_SETUP_ALERT,
                newBooleanInstance(CONF_GROUP_WORKPOOL, n++, WORKPOOL_SHOW_SETUP_ALERT, false));

        /** Debug **/
        builder.put(DEBUG_JAVASCRIPT_LOGGING,
                newBooleanInstance(CONF_GROUP_DEBUG, n++, DEBUG_JAVASCRIPT_LOGGING, false));
        builder.put(DEBUG_JSON_LOGGING, newBooleanInstance(CONF_GROUP_DEBUG, n++, DEBUG_JSON_LOGGING, false));
        builder.put(DEBUG_WEBUI_UI_DELAY,
                newLongInstance(CONF_GROUP_DEBUG, n++, DEBUG_WEBUI_UI_DELAY, false, "ms"));
        builder.put(DEBUG_WEBUI_API_DELAY,
                newLongInstance(CONF_GROUP_DEBUG, n++, DEBUG_WEBUI_API_DELAY, false, "ms"));
        builder.put(DEBUG_WEBUI_SIMS_DELAY,
                newLongInstance(CONF_GROUP_DEBUG, n++, DEBUG_WEBUI_SIMS_DELAY, false, "ms"));

        return builder.build();
    }

    /**
     * This method will be invoked DURING spring initialization
     * because it is needed for setDefaults() to work.  Note that
     * it so happens that _configDao is set already by spring,
     * so we are lucky that this method does not crash.
     */
    private synchronized void initDbMapIfNecessary() {

        if (null != _settingsInDBMap) {
            // already initialized
            return;
        }

        // on a new DB (or on an update), ensure that a row exists for
        // every setting in _PARAM_MAP.
        for (String key : _PARAM_MAP.keySet()) {

            ConfigSetting setting = _configDao.findForKey(key);
            if (null == setting) {
                // add an empty entry
                setting = new ConfigSetting();
                setting.setKey(key);
                setting.setValue(null);
                _configDao.create(setting);
            }
        }

        /* Load all settings from DB */
        _settingsInDBMap = loadFromDB();

        _stringCache = CacheBuilder.newBuilder().concurrencyLevel(20).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return getStringInternal(key);
            }
        });

        _booleanCache = CacheBuilder.newBuilder().concurrencyLevel(20).build(new CacheLoader<String, Boolean>() {
            @Override
            public Boolean load(String key) throws AfNotFoundException, WpException {
                return getBoolInternal(key);
            }
        });

        _intCache = CacheBuilder.newBuilder().concurrencyLevel(20).build(new CacheLoader<String, Integer>() {
            @Override
            public Integer load(String key) throws AfNotFoundException, WpException {
                return getIntegerInternal(key);
            }
        });

        _longCache = CacheBuilder.newBuilder().concurrencyLevel(20).build(new CacheLoader<String, Long>() {
            @Override
            public Long load(String key) {
                return getLongInternal(key);
            }
        });
    }

    /**
     * Load all config settings from the DB.
     *
     * @return a map of config settings <param_key,ConfigSetting>
     */
    private Map<String, ConfigSetting> loadFromDB() {
        final List<ConfigSetting> settingsInDBList = _configDao.findAll();
        return Maps.uniqueIndex(settingsInDBList, new Function<ConfigSetting, String>() {
            /** Key function **/
            @Override
            public String apply(ConfigSetting setting) {
                return setting.getKey();
            }
        });
    }

    /**
     * Construct a list of options for ThinApp runtimes.
     */
    private List<EachOption> getRuntimeOptions() {
        List<ThinAppRuntime> runtimes = getRuntimes();
        Collection<EachOption> rt = Collections2.transform(runtimes, new Function<ThinAppRuntime, EachOption>() {
            @Override
            public EachOption apply(ThinAppRuntime thinAppRuntime) {
                return new SingleOption(String.valueOf(thinAppRuntime.getBuild()), thinAppRuntime.getVersion());
            }
        });
        return new ArrayList<EachOption>(rt);
    }

    // TODO Use a client whose url can be changed at runtime.
    public List<ThinAppRuntime> getRuntimes() {
        ThinAppRuntimeClient runtimeClient = new ThinAppRuntimeClient(getString(CWS_CONVERSIONS_URL));
        // Make a copy so that we can sort it;
        List<ThinAppRuntime> origList = Collections.emptyList();
        try {
            origList = runtimeClient.list();
        } catch (HttpClientErrorException e) {
            _log.error("Could not get list of runtime options, ignoring: " + e);
        }
        List<ThinAppRuntime> runtimes = new ArrayList<ThinAppRuntime>(origList);
        Collections.sort(runtimes, ThinAppRuntime.getBuildComparator());
        return runtimes;
    }

    /**
     * Set the default configuration values from the specified properties
     * file, which should exist in the class path.
     *
     * @param propsFileName Base name of the configuration file.
     */
    public void setDefaults(String propsFileName) {

        initDbMapIfNecessary();

        try {
            /* Convert name=value entries into configuration defaults */
            final ResourceBundle bundle = ResourceBundle.getBundle(propsFileName);
            for (String key : bundle.keySet()) {
                ConfigParam param = findParam(key);
                if (param == null) {
                    _log.warn("Configuration file {} contains invalid key {}", propsFileName, key);
                } else {
                    setValue(param, bundle.getString(key), true);
                }
            }
        } catch (MissingResourceException ex) {
            /* If file is not found, ignore it */
            _log.warn("Configuration file {} not found", propsFileName);
        }
    }

    /**
     * Set a configuration parameter value.
     *
     * @param key
     * @param value
     */
    public void setValue(String key, String value) {
        ConfigParam param = findParam(key);
        if (param == null) {
            throw new IllegalArgumentException("Unknown parameter value=" + key);
        }
        setValue(param, value, false);
    }

    /**
     * Get all parameters plus their current or default values,
     * grouped together by parameter group.
     *
     * @param editable - filter data by editable only.
     * @return All params, mapped by group name.
     */
    public synchronized ConfigGroupMap getGroupMap(boolean editable) {
        ConfigGroupMap map = new ConfigGroupMap();

        for (ConfigParam param : _PARAM_MAP.values()) {
            // if editable only is requested, add only those.
            if (!editable || (editable && param.isUserEditable())) {
                map.add(param, getValue(param));
            }
        }

        return map;
    }

    /**
     * Get the value of a configuration parameter as a boolean.
     *
     * @param key
     * @return Boolean value of 'key'
     */
    public boolean getBool(String key) {
        return _booleanCache.getUnchecked(key);
    }

    private boolean getBoolInternal(String key) {
        String s = getValue(getParam(key));
        return Boolean.valueOf(s);
    }

    /**
     * Get the value of a configuration parameter as a string.
     *
     * @param key
     * @return String value of 'key'
     */
    public String getString(String key) {
        return _stringCache.getUnchecked(key);
    }

    private String getStringInternal(String key) {
        return getValue(getParam(key));
    }

    /**
     * Get the value of a configuration parameter as a long.
     * If the value is not set or is invalid, return 0L
     *
     * @param key
     * @return Long value of 'key' / 0L
     */
    public long getLong(String key) {
        return getLong(key, 0L);
    }

    /**
     * Get the value of a configuration parameter as a long.
     * If the value is not set or is not a long datatype, return
     * the default value thats set.
     *
     * @param key
     * @param defaultIfInvalidValue
     * @return Long value of 'key' / defaultIfInvalidValue
     */
    public long getLong(String key, long defaultIfInvalidValue) {
        Long result = null;
        try {
            result = _longCache.getUnchecked(key);
        } catch (CacheLoader.InvalidCacheLoadException e) {
            // this will happen if the value is not set, so ignore.
        }
        if (null == result) {
            result = defaultIfInvalidValue;
        }
        return result;
    }

    @Nullable
    public Long getLongInternal(String key) {
        String s = null;
        try {
            s = getValue(getParam(key));
            if (StringUtils.hasLength(s)) {
                return Long.valueOf(s).longValue();
            }
        } catch (NumberFormatException e) {
            // Since the input value was invalid, use the default value passed.
            _log.debug("Invalid long value for key[{}] : {} ", key, s);
        } catch (Exception e) {
            // If database access failed, this setting cannot be loaded, use default as below.
            _log.warn("Value lookup failed for key[{}] " + e.getMessage(), e);
        }

        return null;
    }

    /**
     * Get the value of a configuration parameter as an integer.
     * If the value is not set or is invalid, return 0
     *
     * @param key
     * @return Integer value of 'key' / 0
     */
    public int getInteger(String key) {
        return getInteger(key, 0);
    }

    /**
     * Get the value of a configuration parameter as an integer.
     * If the value is not set or is not a integer datatype, return
     * the default value thats set.
     *
     * @param key
     * @param defaultIfInvalidValue
     * @return integer value of 'key' / defaultIfInvalidValue
     */
    public int getInteger(String key, int defaultIfInvalidValue) {
        Integer result = null;
        try {
            result = _intCache.getUnchecked(key);
        } catch (CacheLoader.InvalidCacheLoadException e) {
            // this will happen if the value is not set, so ignore.
        }
        if (null == result) {
            result = defaultIfInvalidValue;
        }
        return result;
    }

    private Integer getIntegerInternal(String key) {
        String s = null;
        try {
            s = getValue(getParam(key));
            if (StringUtils.hasLength(s)) {
                return Integer.valueOf(s).intValue();
            }
        } catch (NumberFormatException e) {
            // Since the input value was invalid, use the default value passed.
            _log.debug("Invalid int value for key[{}] : {} ", key, s);
        } catch (Exception e) {
            // If database access failed, this setting cannot be loaded, use default as below.
            _log.warn("Value lookup failed for key[{}] " + e.getMessage(), e);
        }
        return null;
    }

    /**
     * Get the parameter matching the given key.
     * If the key is invalid, an IllegalArgumentException is thrown.
     *
     * @param key
     * @return Parameter with the given key
     * @throws IllegalArgumentException
     */
    public ConfigParam getParam(String key) throws IllegalArgumentException {
        ConfigParam param = findParam(key);
        if (param == null) {
            throw new IllegalArgumentException("Invalid configuration key " + key);
        }

        return param;
    }

    /**
     * Find the parameter matching the given key.
     * If the key is invalid, return null.
     *
     * @param key
     * @return Parameter with the given key, or null if none.
     */
    public ConfigParam findParam(String key) {
        return _PARAM_MAP.get(key);
    }

    /**
     * Find a value for the named configuration parameter. If no custom value is
     * found, the built-in default is returned.
     *
     * @param param Parameter to query.
     * @return Current value, or a default.
     */
    @Nonnull
    private synchronized String getValue(ConfigParam param) {
        initDbMapIfNecessary();

        ConfigSetting setting = _settingsInDBMap.get(param.getKey());
        if (setting == null) {
            throw new IllegalArgumentException("Unknown param " + param.getKey());
        }
        String value = setting.getValue();
        if (null != value) {
            return value;
        }

        // if value is "null", that means that no value has been saved
        // to the database yet.  Instead of just returning this, we compute
        // a default value here.  Note that attempting to compute this default
        // value has been a source of bugs, as outside of this function we
        // can no longer distinguish between a properly recorded value which
        // is changed only by the result of a controller action (with appropriate
        // change events fired) and a computed value (which may have been returned
        // from a web service) which may change from minute-to-minute with no one
        // noticing.

        switch (param.getType()) {
        case BOOLEAN:
            return "false";
        case INTEGER:
            return "0";
        case LONG:
            return "0";
        case STRING:
            return "";
        case SINGLE_SELECT:
            List<EachOption> vals = param.getOptions().getValues(this);
            if (vals.isEmpty() || vals.get(0) instanceof GroupOption) {
                return "";
            }
            return ((SingleOption) vals.get(0)).getKey();
        }

        throw new RuntimeException(
                String.format("Parameter %s has an unknown type %s", param.getKey(), param.getType().toString()));
    }

    /**
     * Set the value for a given configuration parameter.
     *
     * Due to the fact that default values can change dynamically
     * (as in the case under a fresh install for runtimes, workpools,
     * and datastores), this method will always save the desired
     * value to the database, even if it appears to the the same as
     * the default.  For more info, see bug 837469.
     *
     * @param param a config parameter instance.
     * @param value a new or updated value.
     * @param onlyIfUnset when true, the new value will only be set if the
     *                existing value is null
     */
    private synchronized void setValue(@Nonnull ConfigParam param, @Nonnull String value, boolean onlyIfUnset) {

        initDbMapIfNecessary();

        String key = param.getKey();
        ConfigSetting setting = _settingsInDBMap.get(key);
        if (setting == null) {
            // the setting must have already been created!
            throw new IllegalArgumentException("Unknown setting " + param);
        }

        // we're setting a default value.  Don't overwrite a value
        // that is already present in the db, and log our actions
        // in either case.
        if (onlyIfUnset) {
            if (null == setting.getValue()) {
                _log.debug("Setting default for {}", key);
            } else {
                // don't change what's already there
                _log.debug("Ignoring default for {}, value already defined", key);
                return;
            }
        }

        // note: it is important to save the value passed in, even
        // if we think it is the same as what we already have recorded,
        // because sometimes the value from getValue() is NOT actually
        // what was retrieved from the database.  If the user has made
        // a selection, we want to now store that value in the database,
        // so that the default can't change out from under him or her.
        // Please see the note in setValue() for more info.

        /* Change existing setting */
        setting.setValue(value);
        _configDao.update(setting);
        _settingsInDBMap.get(key).setValue(value);

        _booleanCache.invalidate(key);
        _stringCache.invalidate(key);
        _longCache.invalidate(key);
        _intCache.invalidate(key);
    }

    /**
     * This public method helps with consolidating all calls to 1 place.
     * This will be removed shortly, and hence adding this exception.
     * @return
     */
    public boolean isNewUI() {
        return true;
    }

    /**
     * Returns a ThinAppRuntimeClient
     *
     * We need to create this object each time, rather than use spring injection,
     * because its constructor takes the URL to the conversions web service.  This
     * means that if the URL changes during the lifetime of the app, the old client
     * object will still be calling into the old URL.
     *
     * @return
     * a ThinAppRuntimeClient instance pointing at the CURRENT web service url
     */
    public ThinAppRuntimeClient newThinappRuntimeClient() {
        return new ThinAppRuntimeClient(getString(CWS_CONVERSIONS_URL));
    }

    public long getDefaultRuntime() {
        Long result = getLong(THINAPP_RUNTIME_ID, -1L);

        // that's awful.  We can't start the app without this!
        // in order to continue (to even get to the settings page),
        // try to get the list of available runtime and just pick the
        // most recent one.
        //
        if (-1L == result) {
            // take the last one in the server's list
            List<ThinAppRuntime> runtimes = getRuntimes();
            if (null != runtimes && !runtimes.isEmpty()) {
                ThinAppRuntime volunteer = runtimes.get(runtimes.size() - 1);
                result = volunteer.getId();
            }
        }
        return result;
    }

    /**
     * Get maximum projects allowed for a batch capture request.
     *
     * @return maximum number of projects
     */
    public int getMaxProjectsPerBatch() {
        int defaultValueIfInvalid = 500;
        return getInteger(TASKQ_MAX_PROJECTS_PER_BATCH, defaultValueIfInvalid);
    }

    /**
     * Determine if publishing to Horizon is enabled.
     *
     * @return
     */
    public boolean publishToHorizonEnabled() {
        return getBool(HORIZON_ENABLED);
    }

    /**
     * Get the Horizon URL if Horizon publishing is enabled, otherwise null.
     *
     * @return
     */
    public String getHorizonUrlIfEnabled() {
        return getBool(HORIZON_ENABLED) ? getString(HORIZON_URL) : null;
    }

    /**
     * Implementation of AfConfigParamValues which returns a list of
     * datastore names for the DATASTORE_DEFAULT_OUTPUT configuration parameter.
     */
    public static final class DefaultDatastoreValues implements ConfigParamOptions {
        @Override
        public List<EachOption> getValues(ConfigRegistry reg) {
            List<EachOption> names = new ArrayList<EachOption>();
            try {
                DatastoreClientService dsClient = new DatastoreClientService(reg);
                for (DsDatastore ds : dsClient.getAllDatastores()) {
                    if (ds.getStatus() == Datastore.Status.online) {
                        names.add(new SingleOption(ds.getId().toString(), ds.getName()));
                    }
                }
            } catch (Exception ex) {
                // Ignore
            }
            return names;
        }
    }

    /**
     * Implementation of AfConfigParamValues which returns a list of
     * known UI skins
     */
    public static final class SkinValues implements ConfigParamOptions {
        private static final List<EachOption> SKIN_NAMES = new ArrayList<EachOption>();

        static {
            SKIN_NAMES.add(new SingleOption("base"));
        }

        @Override
        public List<EachOption> getValues(ConfigRegistry reg) {
            return SKIN_NAMES;
        }
    }

    /**
     * Implementation of AfConfigParamValues which returns a list of
     * known virtual infrastructure types that TAF workpool uses.
     */
    public static final class viTypes implements ConfigParamOptions {
        public enum VIType {
            vc, vcNoClone, wsNoClone
        }

        private static final List<EachOption> VI_TYPES = new ArrayList<EachOption>();

        static {
            VI_TYPES.add(new SingleOption(VIType.vc.name(), "M.CONFIG.VI.TYPE." + VIType.vc.name(), true));
            VI_TYPES.add(
                    new SingleOption(VIType.vcNoClone.name(), "M.CONFIG.VI.TYPE." + VIType.vcNoClone.name(), true));
            VI_TYPES.add(
                    new SingleOption(VIType.wsNoClone.name(), "M.CONFIG.VI.TYPE." + VIType.wsNoClone.name(), true));
        }

        @Override
        public List<EachOption> getValues(ConfigRegistry reg) {
            return VI_TYPES;
        }
    }

    /**
     * Implementation of AfConfigParamValues which returns a list of
     * available workpools
     */
    public static final class WorkpoolValues implements ConfigParamOptions {
        @Override
        public List<EachOption> getValues(ConfigRegistry reg) {
            List<EachOption> names = new ArrayList<EachOption>();
            WorkpoolClientService client = new WorkpoolClientService(reg);
            try {
                List<Workpool> workpools = client.getAllWorkpools();
                for (Workpool workpool : workpools) {
                    names.add(new SingleOption(workpool.getId().toString(), workpool.getName()));
                }
            } catch (WpException ex) {
                // Failed to get workpools; ignore
            }
            return names;
        }
    }

    /**
     * Implementation of AfConfigParamValues which returns a list of
     * known file share directory layouts.
     */
    public static final class DirLayoutValues implements ConfigParamOptions {
        private static final List<EachOption> DIR_LAYOUTS = new ArrayList<EachOption>();

        static {
            DIR_LAYOUTS.add(new SingleOption("vendor/name/version/locale/revision"));
            DIR_LAYOUTS.add(new SingleOption("vendor/name/version/revision/locale"));
            DIR_LAYOUTS.add(new SingleOption("name/vendor/version/locale/revision"));
            DIR_LAYOUTS.add(new SingleOption("name/vendor/version/revision/locale"));
            DIR_LAYOUTS.add(new SingleOption("name/version/locale/revision"));
            DIR_LAYOUTS.add(new SingleOption("name/version/revision/locale"));
            DIR_LAYOUTS.add(new SingleOption("name/locale/version/revision"));
            DIR_LAYOUTS.add(new SingleOption("name/version"));
        }

        @Override
        public List<EachOption> getValues(ConfigRegistry reg) {
            return DIR_LAYOUTS;
        }
    }
}