org.glowroot.central.repo.ConfigRepositoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.central.repo.ConfigRepositoryImpl.java

Source

/*
 * Copyright 2015-2019 the original author or authors.
 *
 * 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 org.glowroot.central.repo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import org.checkerframework.checker.nullness.qual.Nullable;

import org.glowroot.central.repo.AgentConfigDao.AgentConfigUpdater;
import org.glowroot.common.util.Versions;
import org.glowroot.common2.config.AllCentralAdminConfig;
import org.glowroot.common2.config.AllEmbeddedAdminConfig;
import org.glowroot.common2.config.CentralAdminGeneralConfig;
import org.glowroot.common2.config.CentralStorageConfig;
import org.glowroot.common2.config.CentralWebConfig;
import org.glowroot.common2.config.EmbeddedAdminGeneralConfig;
import org.glowroot.common2.config.EmbeddedStorageConfig;
import org.glowroot.common2.config.EmbeddedWebConfig;
import org.glowroot.common2.config.HealthchecksIoConfig;
import org.glowroot.common2.config.HttpProxyConfig;
import org.glowroot.common2.config.ImmutableAllCentralAdminConfig;
import org.glowroot.common2.config.ImmutableCentralAdminGeneralConfig;
import org.glowroot.common2.config.ImmutableCentralStorageConfig;
import org.glowroot.common2.config.ImmutableCentralWebConfig;
import org.glowroot.common2.config.ImmutableHttpProxyConfig;
import org.glowroot.common2.config.ImmutableLdapConfig;
import org.glowroot.common2.config.ImmutablePagerDutyConfig;
import org.glowroot.common2.config.ImmutableRoleConfig;
import org.glowroot.common2.config.ImmutableSlackConfig;
import org.glowroot.common2.config.ImmutableSmtpConfig;
import org.glowroot.common2.config.ImmutableUserConfig;
import org.glowroot.common2.config.LdapConfig;
import org.glowroot.common2.config.MoreConfigDefaults;
import org.glowroot.common2.config.PagerDutyConfig;
import org.glowroot.common2.config.PagerDutyConfig.PagerDutyIntegrationKey;
import org.glowroot.common2.config.RoleConfig;
import org.glowroot.common2.config.SlackConfig;
import org.glowroot.common2.config.SlackConfig.SlackWebhook;
import org.glowroot.common2.config.SmtpConfig;
import org.glowroot.common2.config.StorageConfig;
import org.glowroot.common2.config.UserConfig;
import org.glowroot.common2.config.WebConfig;
import org.glowroot.common2.repo.ConfigRepository;
import org.glowroot.common2.repo.ConfigValidation;
import org.glowroot.common2.repo.util.LazySecretKey;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.AdvancedConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.AlertConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.AlertConfig.AlertCondition;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.GaugeConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.GeneralConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.InstrumentationConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.JvmConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.PluginConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.PluginProperty;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.PluginProperty.Value.ValCase;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.SyntheticMonitorConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.TransactionConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.UiDefaultsConfig;

public class ConfigRepositoryImpl implements ConfigRepository {

    // TODO this needs to be in sync with agents, so have agents pick up value from central
    private static final long GAUGE_COLLECTION_INTERVAL_MILLIS = Long
            .getLong("glowroot.internal.gaugeCollectionIntervalMillis", 5000);

    private final CentralConfigDao centralConfigDao;
    private final AgentConfigDao agentConfigDao;
    private final UserDao userDao;
    private final RoleDao roleDao;

    private final ImmutableList<RollupConfig> rollupConfigs;

    private final LazySecretKey lazySecretKey;

    private final Set<AgentConfigListener> agentConfigListeners = Sets.newCopyOnWriteArraySet();

    ConfigRepositoryImpl(CentralConfigDao centralConfigDao, AgentConfigDao agentConfigDao, UserDao userDao,
            RoleDao roleDao, String symmetricEncryptionKey) {
        this.centralConfigDao = centralConfigDao;
        this.agentConfigDao = agentConfigDao;
        this.userDao = userDao;
        this.roleDao = roleDao;
        rollupConfigs = ImmutableList.copyOf(RollupConfig.buildRollupConfigs());
        lazySecretKey = new LazySecretKeyImpl(symmetricEncryptionKey);

        centralConfigDao.addKeyType(GENERAL_KEY, ImmutableCentralAdminGeneralConfig.class);
        centralConfigDao.addKeyType(WEB_KEY, ImmutableCentralWebConfig.class);
        centralConfigDao.addKeyType(STORAGE_KEY, ImmutableCentralStorageConfig.class);
        centralConfigDao.addKeyType(SMTP_KEY, ImmutableSmtpConfig.class);
        centralConfigDao.addKeyType(HTTP_PROXY_KEY, ImmutableHttpProxyConfig.class);
        centralConfigDao.addKeyType(LDAP_KEY, ImmutableLdapConfig.class);
        centralConfigDao.addKeyType(PAGER_DUTY_KEY, ImmutablePagerDutyConfig.class);
        centralConfigDao.addKeyType(SLACK_KEY, ImmutableSlackConfig.class);
    }

    @Override
    public GeneralConfig getGeneralConfig(String agentRollupId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentRollupId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentRollupId);
        }
        return agentConfig.getGeneralConfig();
    }

    @Override
    public TransactionConfig getTransactionConfig(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getTransactionConfig();
    }

    @Override
    public JvmConfig getJvmConfig(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getJvmConfig();
    }

    // central supports ui config on rollups
    @Override
    public UiDefaultsConfig getUiDefaultsConfig(String agentRollupId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentRollupId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentRollupId);
        }
        return agentConfig.getUiDefaultsConfig();
    }

    // central supports advanced config on rollups (maxQueryAggregates and maxServiceCallAggregates)
    @Override
    public AdvancedConfig getAdvancedConfig(String agentRollupId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentRollupId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentRollupId);
        }
        return agentConfig.getAdvancedConfig();
    }

    @Override
    public List<GaugeConfig> getGaugeConfigs(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getGaugeConfigList();
    }

    @Override
    public GaugeConfig getGaugeConfig(String agentId, String configVersion) throws Exception {
        for (GaugeConfig config : getGaugeConfigs(agentId)) {
            if (Versions.getVersion(config).equals(configVersion)) {
                return config;
            }
        }
        throw new IllegalStateException("Gauge config not found: " + configVersion);
    }

    // central supports synthetic monitor configs on rollups
    @Override
    public List<SyntheticMonitorConfig> getSyntheticMonitorConfigs(String agentRollupId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentRollupId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentRollupId);
        }
        return agentConfig.getSyntheticMonitorConfigList();
    }

    // central supports synthetic monitor configs on rollups
    @Override
    public @Nullable SyntheticMonitorConfig getSyntheticMonitorConfig(String agentRollupId,
            String syntheticMonitorId) throws Exception {
        for (SyntheticMonitorConfig config : getSyntheticMonitorConfigs(agentRollupId)) {
            if (config.getId().equals(syntheticMonitorId)) {
                return config;
            }
        }
        return null;
    }

    // central supports alert configs on rollups
    @Override
    public List<AlertConfig> getAlertConfigs(String agentRollupId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentRollupId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentRollupId);
        }
        return agentConfig.getAlertConfigList();
    }

    // central supports alert configs on rollups
    public List<AlertConfig> getAlertConfigsForSyntheticMonitorId(String agentRollupId, String syntheticMonitorId)
            throws Exception {
        List<AlertConfig> configs = new ArrayList<>();
        for (AlertConfig config : getAlertConfigs(agentRollupId)) {
            AlertCondition alertCondition = config.getCondition();
            if (alertCondition.getValCase() == AlertCondition.ValCase.SYNTHETIC_MONITOR_CONDITION && alertCondition
                    .getSyntheticMonitorCondition().getSyntheticMonitorId().equals(syntheticMonitorId)) {
                configs.add(config);
            }
        }
        return configs;
    }

    // central supports alert configs on rollups
    @Override
    public @Nullable AlertConfig getAlertConfig(String agentRollupId, String configVersion) throws Exception {
        for (AlertConfig config : getAlertConfigs(agentRollupId)) {
            if (Versions.getVersion(config).equals(configVersion)) {
                return config;
            }
        }
        throw new IllegalStateException("Alert config not found: " + configVersion);
    }

    @Override
    public List<PluginConfig> getPluginConfigs(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getPluginConfigList();
    }

    @Override
    public PluginConfig getPluginConfig(String agentId, String pluginId) throws Exception {
        for (PluginConfig config : getPluginConfigs(agentId)) {
            if (config.getId().equals(pluginId)) {
                return config;
            }
        }
        throw new IllegalStateException("Plugin config not found: " + pluginId);
    }

    @Override
    public List<InstrumentationConfig> getInstrumentationConfigs(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getInstrumentationConfigList();
    }

    @Override
    public InstrumentationConfig getInstrumentationConfig(String agentId, String configVersion) throws Exception {
        for (InstrumentationConfig config : getInstrumentationConfigs(agentId)) {
            if (Versions.getVersion(config).equals(configVersion)) {
                return config;
            }
        }
        throw new IllegalStateException("Instrumentation config not found: " + configVersion);
    }

    @Override
    public AgentConfig getAllConfig(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        } else {
            return agentConfig;
        }
    }

    @Override
    public EmbeddedAdminGeneralConfig getEmbeddedAdminGeneralConfig() {
        throw new UnsupportedOperationException();
    }

    @Override
    public ImmutableCentralAdminGeneralConfig getCentralAdminGeneralConfig() throws Exception {
        ImmutableCentralAdminGeneralConfig config = (ImmutableCentralAdminGeneralConfig) centralConfigDao
                .read(GENERAL_KEY);
        if (config == null) {
            return ImmutableCentralAdminGeneralConfig.builder().build();
        }
        return config;
    }

    @Override
    public List<UserConfig> getUserConfigs() throws Exception {
        return userDao.read();
    }

    @Override
    public UserConfig getUserConfig(String username) throws Exception {
        UserConfig config = userDao.read(username);
        if (config == null) {
            throw new UserNotFoundException();
        }
        return config;
    }

    @Override
    public @Nullable UserConfig getUserConfigCaseInsensitive(String username) throws Exception {
        return userDao.readCaseInsensitive(username);
    }

    @Override
    public boolean namedUsersExist() throws Exception {
        return userDao.namedUsersExist();
    }

    @Override
    public List<RoleConfig> getRoleConfigs() throws Exception {
        return roleDao.read();
    }

    @Override
    public @Nullable RoleConfig getRoleConfig(String name) throws Exception {
        return roleDao.read(name);
    }

    @Override
    public WebConfig getWebConfig() throws Exception {
        return getCentralWebConfig();
    }

    @Override
    public EmbeddedWebConfig getEmbeddedWebConfig() {
        throw new UnsupportedOperationException();
    }

    @Override
    public CentralWebConfig getCentralWebConfig() throws Exception {
        CentralWebConfig config = (CentralWebConfig) centralConfigDao.read(WEB_KEY);
        if (config == null) {
            return ImmutableCentralWebConfig.builder().build();
        }
        return config;
    }

    @Override
    public StorageConfig getStorageConfig() throws Exception {
        return getCentralStorageConfig();
    }

    @Override
    public EmbeddedStorageConfig getEmbeddedStorageConfig() {
        throw new UnsupportedOperationException();
    }

    @Override
    public CentralStorageConfig getCentralStorageConfig() throws Exception {
        CentralStorageConfig config = (CentralStorageConfig) centralConfigDao.read(STORAGE_KEY);
        if (config == null) {
            return ImmutableCentralStorageConfig.builder().build();
        }
        if (config.hasListIssues()) {
            return withCorrectedLists(config);
        }
        return config;
    }

    @Override
    public SmtpConfig getSmtpConfig() throws Exception {
        SmtpConfig config = (SmtpConfig) centralConfigDao.read(SMTP_KEY);
        if (config == null) {
            return ImmutableSmtpConfig.builder().build();
        }
        return config;
    }

    @Override
    public HttpProxyConfig getHttpProxyConfig() throws Exception {
        HttpProxyConfig config = (HttpProxyConfig) centralConfigDao.read(HTTP_PROXY_KEY);
        if (config == null) {
            return ImmutableHttpProxyConfig.builder().build();
        }
        return config;
    }

    @Override
    public LdapConfig getLdapConfig() throws Exception {
        LdapConfig config = (LdapConfig) centralConfigDao.read(LDAP_KEY);
        if (config == null) {
            return ImmutableLdapConfig.builder().build();
        }
        return config;
    }

    @Override
    public PagerDutyConfig getPagerDutyConfig() throws Exception {
        PagerDutyConfig config = (PagerDutyConfig) centralConfigDao.read(PAGER_DUTY_KEY);
        if (config == null) {
            return ImmutablePagerDutyConfig.builder().build();
        }
        return config;
    }

    @Override
    public SlackConfig getSlackConfig() throws Exception {
        SlackConfig config = (SlackConfig) centralConfigDao.read(SLACK_KEY);
        if (config == null) {
            return ImmutableSlackConfig.builder().build();
        }
        return config;
    }

    @Override
    public HealthchecksIoConfig getHealthchecksIoConfig() {
        throw new UnsupportedOperationException();
    }

    @Override
    public AllEmbeddedAdminConfig getAllEmbeddedAdminConfig() {
        throw new UnsupportedOperationException();
    }

    @Override
    public AllCentralAdminConfig getAllCentralAdminConfig() throws Exception {
        ImmutableAllCentralAdminConfig.Builder builder = ImmutableAllCentralAdminConfig.builder()
                .general(getCentralAdminGeneralConfig());
        for (UserConfig userConfig : getUserConfigs()) {
            builder.addUsers(ImmutableUserConfig.copyOf(userConfig));
        }
        for (RoleConfig roleConfig : getRoleConfigs()) {
            builder.addRoles(ImmutableRoleConfig.copyOf(roleConfig));
        }
        return builder.web(ImmutableCentralWebConfig.copyOf(getCentralWebConfig()))
                .storage(ImmutableCentralStorageConfig.copyOf(getCentralStorageConfig()))
                .smtp(ImmutableSmtpConfig.copyOf(getSmtpConfig()))
                .httpProxy(ImmutableHttpProxyConfig.copyOf(getHttpProxyConfig()))
                .ldap(ImmutableLdapConfig.copyOf(getLdapConfig()))
                .pagerDuty(ImmutablePagerDutyConfig.copyOf(getPagerDutyConfig()))
                .slack(ImmutableSlackConfig.copyOf(getSlackConfig())).build();
    }

    @Override
    public boolean isConfigReadOnly(String agentId) throws Exception {
        AgentConfig agentConfig = agentConfigDao.read(agentId);
        if (agentConfig == null) {
            throw new AgentConfigNotFoundException(agentId);
        }
        return agentConfig.getConfigReadOnly();
    }

    @Override
    public void updateGeneralConfig(String agentId, GeneralConfig config, String priorVersion) throws Exception {
        agentConfigDao.updateCentralOnly(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                String existingVersion = Versions.getVersion(agentConfig.getGeneralConfig());
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().setGeneralConfig(config).build();
            }
        });
        // no need to call notifyAgentConfigListeners since updating "central only" data
    }

    @Override
    public void updateTransactionConfig(String agentId, TransactionConfig config, String priorVersion)
            throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                String existingVersion = Versions.getVersion(agentConfig.getTransactionConfig());
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().setTransactionConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void insertGaugeConfig(String agentId, GaugeConfig config) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                // check for duplicate mbeanObjectName
                for (GaugeConfig loopConfig : agentConfig.getGaugeConfigList()) {
                    if (loopConfig.getMbeanObjectName().equals(config.getMbeanObjectName())) {
                        throw new DuplicateMBeanObjectNameException();
                    }
                }
                // no need to check for exact match since redundant with dup mbean object name check
                return agentConfig.toBuilder().addGaugeConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void updateGaugeConfig(String agentId, GaugeConfig config, String priorVersion) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<GaugeConfig> existingConfigs = Lists.newArrayList(agentConfig.getGaugeConfigList());
                ListIterator<GaugeConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    GaugeConfig loopConfig = i.next();
                    String loopVersion = Versions.getVersion(loopConfig);
                    if (loopVersion.equals(priorVersion)) {
                        i.set(config);
                        found = true;
                    } else if (loopConfig.getMbeanObjectName().equals(config.getMbeanObjectName())) {
                        throw new DuplicateMBeanObjectNameException();
                    }
                    // no need to check for exact match since redundant with dup mbean object name
                    // check
                }
                if (!found) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearGaugeConfig().addAllGaugeConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void deleteGaugeConfig(String agentId, String version) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<GaugeConfig> existingConfigs = Lists.newArrayList(agentConfig.getGaugeConfigList());
                ListIterator<GaugeConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    if (Versions.getVersion(i.next()).equals(version)) {
                        i.remove();
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearGaugeConfig().addAllGaugeConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void updateJvmConfig(String agentId, JvmConfig config, String priorVersion) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                String existingVersion = Versions.getVersion(agentConfig.getJvmConfig());
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().setJvmConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    // central supports synthetic monitor configs on rollups
    @Override
    public void insertSyntheticMonitorConfig(String agentRollupId, SyntheticMonitorConfig config) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                // check for duplicate display
                String display = MoreConfigDefaults.getDisplayOrDefault(config);
                for (SyntheticMonitorConfig loopConfig : agentConfig.getSyntheticMonitorConfigList()) {
                    if (MoreConfigDefaults.getDisplayOrDefault(loopConfig).equals(display)) {
                        throw new DuplicateSyntheticMonitorDisplayException();
                    }
                }
                // no need to check for exact match since redundant with duplicate name check
                return agentConfig.toBuilder().addSyntheticMonitorConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports synthetic monitor configs on rollups
    @Override
    public void updateSyntheticMonitorConfig(String agentRollupId, SyntheticMonitorConfig config,
            String priorVersion) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<SyntheticMonitorConfig> existingConfigs = Lists
                        .newArrayList(agentConfig.getSyntheticMonitorConfigList());
                ListIterator<SyntheticMonitorConfig> i = existingConfigs.listIterator();
                boolean found = false;
                String display = config.getDisplay();
                while (i.hasNext()) {
                    SyntheticMonitorConfig loopConfig = i.next();
                    if (loopConfig.getId().equals(config.getId())) {
                        if (!Versions.getVersion(loopConfig).equals(priorVersion)) {
                            throw new OptimisticLockException();
                        }
                        i.set(config);
                        found = true;
                    } else if (MoreConfigDefaults.getDisplayOrDefault(loopConfig).equals(display)) {
                        throw new DuplicateSyntheticMonitorDisplayException();
                    }
                }
                if (!found) {
                    throw new SyntheticNotFoundException();
                }
                return agentConfig.toBuilder().clearSyntheticMonitorConfig()
                        .addAllSyntheticMonitorConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports synthetic monitor configs on rollups
    @Override
    public void deleteSyntheticMonitorConfig(String agentRollupId, String syntheticMonitorId) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                if (!getAlertConfigsForSyntheticMonitorId(agentRollupId, syntheticMonitorId).isEmpty()) {
                    throw new IllegalStateException(
                            "Cannot delete synthetic monitor is being used by active alert");
                }
                List<SyntheticMonitorConfig> existingConfigs = Lists
                        .newArrayList(agentConfig.getSyntheticMonitorConfigList());
                ListIterator<SyntheticMonitorConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    if (i.next().getId().equals(syntheticMonitorId)) {
                        i.remove();
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearSyntheticMonitorConfig()
                        .addAllSyntheticMonitorConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports alert configs on rollups
    @Override
    public void insertAlertConfig(String agentRollupId, AlertConfig config) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                for (AlertConfig loopConfig : agentConfig.getAlertConfigList()) {
                    if (loopConfig.getCondition().equals(config.getCondition())) {
                        throw new IllegalStateException("This exact alert condition already exists");
                    }
                }
                return agentConfig.toBuilder().addAlertConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports alert configs on rollups
    @Override
    public void updateAlertConfig(String agentRollupId, AlertConfig config, String priorVersion) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<AlertConfig> existingConfigs = Lists.newArrayList(agentConfig.getAlertConfigList());
                ListIterator<AlertConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    AlertConfig loopConfig = i.next();
                    if (Versions.getVersion(loopConfig).equals(priorVersion)) {
                        i.set(config);
                        found = true;
                    } else if (loopConfig.getCondition().equals(config.getCondition())) {
                        throw new IllegalStateException("This exact alert condition already exists");
                    }
                }
                if (!found) {
                    throw new AlertNotFoundException();
                }
                return agentConfig.toBuilder().clearAlertConfig().addAllAlertConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports alert configs on rollups
    @Override
    public void deleteAlertConfig(String agentRollupId, String version) throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<AlertConfig> existingConfigs = Lists.newArrayList(agentConfig.getAlertConfigList());
                ListIterator<AlertConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    if (Versions.getVersion(i.next()).equals(version)) {
                        i.remove();
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearAlertConfig().addAllAlertConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    // central supports ui config on rollups
    @Override
    public void updateUiDefaultsConfig(String agentRollupId, UiDefaultsConfig config, String priorVersion)
            throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                String existingVersion = Versions.getVersion(agentConfig.getUiDefaultsConfig());
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().setUiDefaultsConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    @Override
    public void updatePluginConfig(String agentId, PluginConfig config, String priorVersion) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<PluginConfig> pluginConfigs = buildPluginConfigs(config, priorVersion, agentConfig);
                return agentConfig.toBuilder().clearPluginConfig().addAllPluginConfig(pluginConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void insertInstrumentationConfig(String agentId, InstrumentationConfig config) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                if (agentConfig.getInstrumentationConfigList().contains(config)) {
                    throw new IllegalStateException("This exact instrumentation already exists");
                }
                return agentConfig.toBuilder().addInstrumentationConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void updateInstrumentationConfig(String agentId, InstrumentationConfig config, String priorVersion)
            throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                String newVersion = Versions.getVersion(config);
                List<InstrumentationConfig> existingConfigs = Lists
                        .newArrayList(agentConfig.getInstrumentationConfigList());
                ListIterator<InstrumentationConfig> i = existingConfigs.listIterator();
                boolean found = false;
                while (i.hasNext()) {
                    String loopVersion = Versions.getVersion(i.next());
                    if (loopVersion.equals(priorVersion)) {
                        i.set(config);
                        found = true;
                    } else if (loopVersion.equals(newVersion)) {
                        throw new IllegalStateException("This exact instrumentation already exists");
                    }
                }
                if (!found) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearInstrumentationConfig()
                        .addAllInstrumentationConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void deleteInstrumentationConfigs(String agentId, List<String> versions) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                List<InstrumentationConfig> existingConfigs = Lists
                        .newArrayList(agentConfig.getInstrumentationConfigList());
                ListIterator<InstrumentationConfig> i = existingConfigs.listIterator();
                List<String> remainingVersions = Lists.newArrayList(versions);
                while (i.hasNext()) {
                    String currVersion = Versions.getVersion(i.next());
                    if (remainingVersions.contains(currVersion)) {
                        i.remove();
                        remainingVersions.remove(currVersion);
                    }
                }
                if (!remainingVersions.isEmpty()) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().clearInstrumentationConfig()
                        .addAllInstrumentationConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    // ignores any instrumentation configs that are duplicates of existing instrumentation configs
    @Override
    public void insertInstrumentationConfigs(String agentId, List<InstrumentationConfig> configs) throws Exception {
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                AgentConfig.Builder builder = agentConfig.toBuilder();
                List<InstrumentationConfig> existingConfigs = Lists
                        .newArrayList(agentConfig.getInstrumentationConfigList());
                for (InstrumentationConfig config : configs) {
                    if (!existingConfigs.contains(config)) {
                        existingConfigs.add(config);
                    }
                }
                return builder.clearInstrumentationConfig().addAllInstrumentationConfig(existingConfigs).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void updateAdvancedConfig(String agentRollupId, AdvancedConfig config, String priorVersion)
            throws Exception {
        agentConfigDao.update(agentRollupId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws OptimisticLockException {
                String existingVersion = Versions.getVersion(agentConfig.getAdvancedConfig());
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                return agentConfig.toBuilder().setAdvancedConfig(config).build();
            }
        });
        notifyAgentConfigListeners(agentRollupId);
    }

    @Override
    public void updateAllConfig(String agentId, AgentConfig config, @Nullable String priorVersion)
            throws Exception {
        ConfigValidation.validatePartOne(config);
        agentConfigDao.update(agentId, new AgentConfigUpdater() {
            @Override
            public AgentConfig updateAgentConfig(AgentConfig agentConfig) throws Exception {
                if (priorVersion != null) {
                    String existingVersion = Versions.getVersion(agentConfig);
                    if (!priorVersion.equals(existingVersion)) {
                        throw new OptimisticLockException();
                    }
                }
                Set<String> validPluginIds = Sets.newHashSet();
                for (PluginConfig pluginConfig : agentConfig.getPluginConfigList()) {
                    validPluginIds.add(pluginConfig.getId());
                }
                ConfigValidation.validatePartTwo(config, validPluginIds);
                return config.toBuilder().clearPluginConfig()
                        .addAllPluginConfig(buildPluginConfigs(config.getPluginConfigList(), agentConfig)).build();
            }
        });
        notifyAgentConfigListeners(agentId);
    }

    @Override
    public void updateEmbeddedAdminGeneralConfig(EmbeddedAdminGeneralConfig config, String priorVersion) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void updateCentralAdminGeneralConfig(CentralAdminGeneralConfig config, String priorVersion)
            throws Exception {
        centralConfigDao.write(GENERAL_KEY, config, priorVersion);
    }

    @Override
    public void insertUserConfig(UserConfig config) throws Exception {
        // check for case-insensitive duplicate
        String username = config.username();
        for (UserConfig loopConfig : userDao.read()) {
            if (loopConfig.username().equalsIgnoreCase(username)) {
                throw new DuplicateUsernameException();
            }
        }
        userDao.insertIfNotExists(config);
    }

    @Override
    public void updateUserConfig(UserConfig config, String priorVersion) throws Exception {
        UserConfig existingConfig = userDao.read(config.username());
        if (existingConfig == null) {
            throw new UserNotFoundException();
        }
        if (!existingConfig.version().equals(priorVersion)) {
            throw new OptimisticLockException();
        }
        userDao.insert(config);
    }

    @Override
    public void deleteUserConfig(String username) throws Exception {
        boolean found = false;
        List<UserConfig> configs = userDao.read();
        for (UserConfig config : configs) {
            if (config.username().equalsIgnoreCase(username)) {
                found = true;
                break;
            }
        }
        if (!found) {
            throw new UserNotFoundException();
        }
        if (getSmtpConfig().host().isEmpty() && configs.size() == 1) {
            throw new CannotDeleteLastUserException();
        }
        userDao.delete(username);
    }

    @Override
    public void insertRoleConfig(RoleConfig config) throws Exception {
        // check for case-insensitive duplicate
        String name = config.name();
        for (RoleConfig loopConfig : roleDao.read()) {
            if (loopConfig.name().equalsIgnoreCase(name)) {
                throw new DuplicateRoleNameException();
            }
        }
        roleDao.insertIfNotExists(config);
    }

    @Override
    public void updateRoleConfig(RoleConfig config, String priorVersion) throws Exception {
        RoleConfig existingConfig = roleDao.read(config.name());
        if (existingConfig == null) {
            throw new RoleNotFoundException();
        }
        if (!existingConfig.version().equals(priorVersion)) {
            throw new OptimisticLockException();
        }
        roleDao.insert(config);
    }

    @Override
    public void deleteRoleConfig(String name) throws Exception {
        boolean found = false;
        List<RoleConfig> configs = roleDao.read();
        for (RoleConfig config : configs) {
            if (config.name().equalsIgnoreCase(name)) {
                found = true;
                break;
            }
        }
        if (!found) {
            throw new RoleNotFoundException();
        }
        if (configs.size() == 1) {
            throw new CannotDeleteLastRoleException();
        }
        roleDao.delete(name);
    }

    @Override
    public void updateEmbeddedWebConfig(EmbeddedWebConfig config, String priorVersion) throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public void updateCentralWebConfig(CentralWebConfig config, String priorVersion) throws Exception {
        centralConfigDao.write(WEB_KEY, config, priorVersion);
    }

    @Override
    public void updateEmbeddedStorageConfig(EmbeddedStorageConfig config, String priorVersion) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void updateCentralStorageConfig(CentralStorageConfig config, String priorVersion) throws Exception {
        centralConfigDao.write(STORAGE_KEY, config, priorVersion);
    }

    @Override
    public void updateSmtpConfig(SmtpConfig config, String priorVersion) throws Exception {
        centralConfigDao.write(SMTP_KEY, config, priorVersion);
    }

    @Override
    public void updateHttpProxyConfig(HttpProxyConfig config, String priorVersion) throws Exception {
        centralConfigDao.write(HTTP_PROXY_KEY, config, priorVersion);
    }

    @Override
    public void updateLdapConfig(LdapConfig config, String priorVersion) throws Exception {
        centralConfigDao.write(LDAP_KEY, config, priorVersion);
    }

    @Override
    public void updatePagerDutyConfig(PagerDutyConfig config, String priorVersion) throws Exception {
        validatePagerDutyConfig(config);
        centralConfigDao.write(PAGER_DUTY_KEY, config, priorVersion);
    }

    @Override
    public void updateSlackConfig(SlackConfig config, String priorVersion) throws Exception {
        validateSlackConfig(config);
        centralConfigDao.write(SLACK_KEY, config, priorVersion);
    }

    @Override
    public void updateHealthchecksIoConfig(HealthchecksIoConfig healthchecksIoConfig, String priorVersion)
            throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public void updateAllEmbeddedAdminConfig(AllEmbeddedAdminConfig config, @Nullable String priorVersion) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void updateAllCentralAdminConfig(AllCentralAdminConfig config, @Nullable String priorVersion)
            throws Exception {
        validatePagerDutyConfig(config.pagerDuty());
        validateSlackConfig(config.slack());
        if (priorVersion == null) {
            centralConfigDao.writeWithoutOptimisticLocking(GENERAL_KEY, config.general());
            centralConfigDao.writeWithoutOptimisticLocking(WEB_KEY, config.web());
            centralConfigDao.writeWithoutOptimisticLocking(STORAGE_KEY, config.storage());
            centralConfigDao.writeWithoutOptimisticLocking(SMTP_KEY, config.smtp());
            centralConfigDao.writeWithoutOptimisticLocking(HTTP_PROXY_KEY, config.httpProxy());
            centralConfigDao.writeWithoutOptimisticLocking(LDAP_KEY, config.ldap());
            centralConfigDao.writeWithoutOptimisticLocking(PAGER_DUTY_KEY, config.pagerDuty());
            centralConfigDao.writeWithoutOptimisticLocking(SLACK_KEY, config.slack());
            writeUsersWithoutOptimisticLocking(config.users());
            writeRolesWithoutOptimisticLocking(config.roles());
        } else {
            AllCentralAdminConfig currConfig = getAllCentralAdminConfig();
            if (!priorVersion.equals(currConfig.version())) {
                throw new OptimisticLockException();
            }
            centralConfigDao.write(GENERAL_KEY, config.general(), currConfig.general().version());
            centralConfigDao.write(WEB_KEY, config.web(), currConfig.web().version());
            centralConfigDao.write(STORAGE_KEY, config.storage(), currConfig.storage().version());
            centralConfigDao.write(SMTP_KEY, config.smtp(), currConfig.smtp().version());
            centralConfigDao.write(HTTP_PROXY_KEY, config.httpProxy(), currConfig.httpProxy().version());
            centralConfigDao.write(LDAP_KEY, config.ldap(), currConfig.ldap().version());
            centralConfigDao.write(PAGER_DUTY_KEY, config.pagerDuty(), currConfig.pagerDuty().version());
            centralConfigDao.write(SLACK_KEY, config.slack(), currConfig.slack().version());
            // there is currently no optimistic locking when updating users
            writeUsersWithoutOptimisticLocking(config.users());
            writeRolesWithoutOptimisticLocking(config.roles());
        }
    }

    @Override
    public long getGaugeCollectionIntervalMillis() {
        return GAUGE_COLLECTION_INTERVAL_MILLIS;
    }

    @Override
    public ImmutableList<RollupConfig> getRollupConfigs() {
        return rollupConfigs;
    }

    @Override
    public LazySecretKey getLazySecretKey() throws Exception {
        return lazySecretKey;
    }

    public void addAgentConfigListener(AgentConfigListener listener) {
        agentConfigListeners.add(listener);
    }

    private void writeUsersWithoutOptimisticLocking(List<ImmutableUserConfig> userConfigs) throws Exception {
        Map<String, UserConfig> remainingUserConfigs = new HashMap<>();
        for (UserConfig userConfig : getUserConfigs()) {
            remainingUserConfigs.put(userConfig.username(), userConfig);
        }
        for (UserConfig userConfig : userConfigs) {
            UserConfig existingUserConfig = remainingUserConfigs.remove(userConfig.username());
            if (userConfig.passwordHash().isEmpty() && !userConfig.ldap()) {
                if (existingUserConfig == null) {
                    throw new IllegalStateException("New user " + userConfig.username() + " is missing password");
                }
                userConfig = ImmutableUserConfig.copyOf(userConfig)
                        .withPasswordHash(existingUserConfig.passwordHash());
            }
            userDao.insert(userConfig);
        }
        for (String remainingUsername : remainingUserConfigs.keySet()) {
            userDao.delete(remainingUsername);
        }
    }

    private void writeRolesWithoutOptimisticLocking(List<ImmutableRoleConfig> roleConfigs) throws Exception {
        Map<String, RoleConfig> remainingRoleConfigs = new HashMap<>();
        for (RoleConfig roleConfig : getRoleConfigs()) {
            remainingRoleConfigs.put(roleConfig.name(), roleConfig);
        }
        for (RoleConfig roleConfig : roleConfigs) {
            remainingRoleConfigs.remove(roleConfig.name());
            roleDao.insert(roleConfig);
        }
        for (String remainingRolename : remainingRoleConfigs.keySet()) {
            roleDao.delete(remainingRolename);
        }
    }

    // the updated config is not passed to the listeners to avoid the race condition of multiple
    // config updates being sent out of order, instead listeners must call get*Config() which will
    // never return the updates out of order (at worst it may return the most recent update twice
    // which is ok)
    private void notifyAgentConfigListeners(String agentRollupId) throws Exception {
        for (AgentConfigListener agentConfigListener : agentConfigListeners) {
            agentConfigListener.onChange(agentRollupId);
        }
    }

    private static List<PluginConfig> buildPluginConfigs(PluginConfig updatedConfig, String priorVersion,
            AgentConfig agentConfig) throws OptimisticLockException {
        List<PluginConfig> pluginConfigs = Lists.newArrayList(agentConfig.getPluginConfigList());
        ListIterator<PluginConfig> i = pluginConfigs.listIterator();
        boolean found = false;
        while (i.hasNext()) {
            PluginConfig pluginConfig = i.next();
            if (pluginConfig.getId().equals(updatedConfig.getId())) {
                String existingVersion = Versions.getVersion(pluginConfig);
                if (!priorVersion.equals(existingVersion)) {
                    throw new OptimisticLockException();
                }
                i.set(buildPluginConfig(pluginConfig, updatedConfig.getPropertyList(), true));
                found = true;
                break;
            }
        }
        if (found) {
            return pluginConfigs;
        } else {
            throw new IllegalStateException("Plugin config not found: " + updatedConfig.getId());
        }
    }

    private static List<PluginConfig> buildPluginConfigs(List<PluginConfig> newConfigs, AgentConfig agentConfig) {
        List<PluginConfig> pluginConfigs = new ArrayList<>();
        Map<String, PluginConfig> remainingNewConfigs = new HashMap<>();
        for (PluginConfig newConfig : newConfigs) {
            remainingNewConfigs.put(newConfig.getId(), newConfig);
        }
        for (PluginConfig pluginConfig : agentConfig.getPluginConfigList()) {
            PluginConfig newConfig = remainingNewConfigs.remove(pluginConfig.getId());
            List<PluginProperty> newProperties;
            if (newConfig == null) {
                newProperties = new ArrayList<>();
            } else {
                newProperties = newConfig.getPropertyList();
            }
            pluginConfigs.add(buildPluginConfig(pluginConfig, newProperties, false));
        }
        if (remainingNewConfigs.isEmpty()) {
            return pluginConfigs;
        } else {
            throw new IllegalStateException(
                    "Plugin config(s) not found: " + Joiner.on(", ").join(remainingNewConfigs.keySet()));
        }
    }

    private static PluginConfig buildPluginConfig(PluginConfig existingConfig, List<PluginProperty> newProperties,
            boolean errorOnMissingProperty) {
        Map<String, PluginProperty> newProps = buildMutablePropertiesMap(newProperties);
        PluginConfig.Builder builder = PluginConfig.newBuilder().setId(existingConfig.getId())
                .setName(existingConfig.getName());
        for (PluginProperty existingProperty : existingConfig.getPropertyList()) {
            PluginProperty prop = newProps.remove(existingProperty.getName());
            if (prop == null) {
                if (errorOnMissingProperty) {
                    throw new IllegalStateException("Missing plugin property name: " + existingProperty.getName());
                } else {
                    builder.addProperty(existingProperty.toBuilder().setValue(existingProperty.getDefault()));
                    continue;
                }
            }
            if (!isSameType(prop.getValue(), existingProperty.getValue())) {
                throw new IllegalStateException("Plugin property " + prop.getName() + " has incorrect type: "
                        + prop.getValue().getValCase());
            }
            builder.addProperty(existingProperty.toBuilder().setValue(prop.getValue()));
        }
        if (!newProps.isEmpty()) {
            throw new IllegalStateException(
                    "Unexpected property name(s): " + Joiner.on(", ").join(newProps.keySet()));
        }
        return builder.build();
    }

    private static Map<String, PluginProperty> buildMutablePropertiesMap(List<PluginProperty> properties) {
        return Maps.newHashMap(Maps.uniqueIndex(properties, PluginProperty::getName));
    }

    private static boolean isSameType(PluginProperty.Value left, PluginProperty.Value right) {
        if (left.getValCase() == ValCase.DVAL && right.getValCase() == ValCase.DVAL_NULL) {
            return true;
        }
        if (left.getValCase() == ValCase.DVAL_NULL && right.getValCase() == ValCase.DVAL) {
            return true;
        }
        return left.getValCase() == right.getValCase();
    }

    private static CentralStorageConfig withCorrectedLists(CentralStorageConfig config) {
        CentralStorageConfig defaultConfig = ImmutableCentralStorageConfig.builder().build();
        return ImmutableCentralStorageConfig.builder().copyFrom(config)
                .rollupExpirationHours(fix(config.rollupExpirationHours(), defaultConfig.rollupExpirationHours()))
                .queryAndServiceCallRollupExpirationHours(fix(config.queryAndServiceCallRollupExpirationHours(),
                        defaultConfig.queryAndServiceCallRollupExpirationHours()))
                .profileRollupExpirationHours(
                        fix(config.profileRollupExpirationHours(), defaultConfig.profileRollupExpirationHours()))
                .build();
    }

    private static ImmutableList<Integer> fix(ImmutableList<Integer> thisList, List<Integer> defaultList) {
        if (thisList.size() >= defaultList.size()) {
            return thisList.subList(0, defaultList.size());
        }
        List<Integer> correctedList = Lists.newArrayList(thisList);
        for (int i = thisList.size(); i < defaultList.size(); i++) {
            correctedList.add(defaultList.get(i));
        }
        return ImmutableList.copyOf(correctedList);
    }

    private static void validatePagerDutyConfig(PagerDutyConfig config) throws Exception {
        Set<String> integrationKeys = new HashSet<>();
        Set<String> integrationDisplays = new HashSet<>();
        for (PagerDutyIntegrationKey integrationKey : config.integrationKeys()) {
            if (!integrationKeys.add(integrationKey.key())) {
                throw new DuplicatePagerDutyIntegrationKeyException();
            }
            if (!integrationDisplays.add(integrationKey.display())) {
                throw new DuplicatePagerDutyIntegrationKeyDisplayException();
            }
        }
    }

    private static void validateSlackConfig(SlackConfig config) throws Exception {
        Set<String> webhookUrls = Sets.newHashSet();
        Set<String> webhookDisplays = Sets.newHashSet();
        for (SlackWebhook webhook : config.webhooks()) {
            if (!webhookUrls.add(webhook.url())) {
                throw new DuplicateSlackWebhookUrlException();
            }
            if (!webhookDisplays.add(webhook.display())) {
                throw new DuplicateSlackWebhookDisplayException();
            }
        }
    }

    public interface AgentConfigListener {

        // the new config is not passed to onChange so that the receiver has to get the latest,
        // this avoids race condition worries that two updates may get sent to the receiver in the
        // wrong order
        void onChange(String agentRollupId) throws Exception;
    }

    public static class LazySecretKeyImpl implements LazySecretKey {

        private final @Nullable SecretKey secretKey;

        public LazySecretKeyImpl(String symmetricEncryptionKey) {
            if (symmetricEncryptionKey.isEmpty()) {
                secretKey = null;
            } else {
                byte[] bytes = BaseEncoding.base16().decode(symmetricEncryptionKey.toUpperCase(Locale.ENGLISH));
                secretKey = new SecretKeySpec(bytes, "AES");
            }
        }

        @Override
        public @Nullable SecretKey getExisting() throws Exception {
            return secretKey;
        }

        @Override
        public SecretKey getOrCreate() throws Exception {
            if (secretKey == null) {
                throw new SymmetricEncryptionKeyMissingException();
            }
            return secretKey;
        }
    }
}