/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.cassandra.tools;

import java.io.IOException;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.RuntimeMXBean;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.ConnectException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.annotation.Nullable;
import javax.management.InstanceNotFoundException;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.TabularData;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.rmi.ssl.SslRMIClientSocketFactory;

import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Uninterruptibles;

import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogManagerMBean;
import org.apache.cassandra.audit.AuditLogOptions;
import org.apache.cassandra.audit.AuditLogOptionsCompositeData;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.AuthCacheMBean;
import org.apache.cassandra.auth.CIDRGroupsMappingManager;
import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
import org.apache.cassandra.auth.CIDRPermissionsManager;
import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
import org.apache.cassandra.auth.NetworkPermissionsCache;
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.auth.PermissionsCache;
import org.apache.cassandra.auth.PermissionsCacheMBean;
import org.apache.cassandra.auth.RolesCache;
import org.apache.cassandra.auth.RolesCacheMBean;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
import org.apache.cassandra.db.compression.CompressionDictionaryDetailsTabularData;
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
import org.apache.cassandra.db.compression.TrainingState;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.GuardrailsMBean;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
import org.apache.cassandra.fql.FullQueryLoggerOptions;
import org.apache.cassandra.fql.FullQueryLoggerOptionsCompositeData;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.FailureDetectorMBean;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.GossiperMBean;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.hints.HintsServiceMBean;
import org.apache.cassandra.index.sai.metrics.IndexGroupMetrics;
import org.apache.cassandra.index.sai.metrics.TableQueryMetrics;
import org.apache.cassandra.index.sai.metrics.TableStateMetrics;
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
import org.apache.cassandra.locator.LocationInfoMBean;
import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.metrics.ThreadPoolMetrics;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MessagingServiceMBean;
import org.apache.cassandra.profiler.AsyncProfilerMBean;
import org.apache.cassandra.service.ActiveRepairServiceMBean;
import org.apache.cassandra.service.AsyncProfilerService;
import org.apache.cassandra.service.AutoRepairService;
import org.apache.cassandra.service.AutoRepairServiceMBean;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.CacheServiceMBean;
import org.apache.cassandra.service.GCInspector;
import org.apache.cassandra.service.GCInspectorMXBean;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageProxyMBean;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.service.accord.AccordOperations;
import org.apache.cassandra.service.accord.AccordOperationsMBean;
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.streaming.StreamState;
import org.apache.cassandra.streaming.management.StreamStateCompositeData;
import org.apache.cassandra.tcm.CMSOperations;
import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.tools.RepairRunner.RepairCmd;
import org.apache.cassandra.tools.nodetool.GetTimeout;
import org.apache.cassandra.utils.NativeLibrary;

import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
import static org.apache.cassandra.config.CassandraRelevantProperties.SSL_ENABLE;

/**
 * JMX client operations for Cassandra.
 */
public class NodeProbe implements AutoCloseable
{
    private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
    private static final String ssObjName = "org.apache.cassandra.db:type=StorageService";
    public static final int defaultPort = 7199;

    static long JMX_NOTIFICATION_POLL_INTERVAL_SECONDS = NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS.getLong();

    final String host;
    final int port;
    private String username;
    private String password;


    protected JMXConnector jmxc;
    protected MBeanServerConnection mbeanServerConn;
    protected CompactionManagerMBean compactionProxy;
    protected StorageServiceMBean ssProxy;
    protected SnapshotManagerMBean snapshotProxy;
    protected CMSOperationsMBean cmsProxy;
    protected AccordOperationsMBean accordProxy;
    protected GossiperMBean gossProxy;
    protected MemoryMXBean memProxy;
    protected GCInspectorMXBean gcProxy;
    protected RuntimeMXBean runtimeProxy;
    protected StreamManagerMBean streamProxy;
    protected MessagingServiceMBean msProxy;
    protected FailureDetectorMBean fdProxy;
    protected CacheServiceMBean cacheService;
    protected StorageProxyMBean spProxy;
    protected HintsServiceMBean hsProxy;
    protected BatchlogManagerMBean bmProxy;
    protected ActiveRepairServiceMBean arsProxy;
    protected AuditLogManagerMBean almProxy;
    protected PasswordAuthenticator.CredentialsCacheMBean ccProxy;
    protected AuthorizationProxy.JmxPermissionsCacheMBean jpcProxy;
    protected NetworkPermissionsCacheMBean npcProxy;
    protected CIDRPermissionsManagerMBean cpbProxy;
    protected CIDRGroupsMappingManagerMBean cmbProxy;
    protected PermissionsCacheMBean pcProxy;
    protected RolesCacheMBean rcProxy;
    protected AutoRepairServiceMBean autoRepairProxy;
    protected AsyncProfilerMBean asyncProfilerProxy;
    protected GuardrailsMBean grProxy;
    protected volatile Output output;

    protected CIDRFilteringMetricsTableMBean cfmProxy;

    /**
     * Creates a NodeProbe using the specified JMX host, port, username, and password.
     *
     * @param host hostname or IP address of the JMX agent
     * @param port TCP port of the remote JMX agent
     * @throws IOException on connection failures
     */
    public NodeProbe(String host, int port, String username, String password) throws IOException
    {
        assert username != null && !username.isEmpty() && password != null && !password.isEmpty()
               : "neither username nor password can be blank";

        this.host = host;
        this.port = port;
        this.username = username;
        this.password = password;
        this.output = Output.CONSOLE;
        connect();
    }

    /**
     * Creates a NodeProbe using the specified JMX host and port.
     *
     * @param host hostname or IP address of the JMX agent
     * @param port TCP port of the remote JMX agent
     * @throws IOException on connection failures
     */
    public NodeProbe(String host, int port) throws IOException
    {
        this.host = host;
        this.port = port;
        this.output = Output.CONSOLE;
        connect();
    }

    /**
     * Creates a NodeProbe using the specified JMX host and default port.
     *
     * @param host hostname or IP address of the JMX agent
     * @throws IOException on connection failures
     */
    public NodeProbe(String host) throws IOException
    {
        this.host = host;
        this.port = defaultPort;
        this.output = Output.CONSOLE;
        connect();
    }

    protected NodeProbe()
    {
        // this constructor is only used for extensions to rewrite their own connect method
        this.host = "";
        this.port = 0;
        this.output = Output.CONSOLE;
    }

    /**
     * Create a connection to the JMX agent and setup the M[X]Bean proxies.
     *
     * @throws IOException on connection failures
     */
    protected void connect() throws IOException
    {
        String host = this.host;
        if (host.contains(":"))
        {
            // Use square brackets to surround IPv6 addresses to fix CASSANDRA-7669 and CASSANDRA-17581
            host = "[" + host + "]";
        }
        JMXServiceURL jmxUrl = new JMXServiceURL(String.format(fmtUrl, host, port));
        Map<String, Object> env = new HashMap<String, Object>();
        if (username != null)
        {
            String[] creds = { username, password };
            env.put(JMXConnector.CREDENTIALS, creds);
        }

        env.put("com.sun.jndi.rmi.factory.socket", getRMIClientSocketFactory());

        jmxc = JMXConnectorFactory.connect(jmxUrl, env);
        mbeanServerConn = jmxc.getMBeanServerConnection();

        try
        {
            ObjectName name = new ObjectName(ssObjName);
            ssProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class);
            name = new ObjectName(SnapshotManagerMBean.MBEAN_NAME);
            snapshotProxy = JMX.newMBeanProxy(mbeanServerConn, name, SnapshotManagerMBean.class);
            name = new ObjectName(CMSOperations.MBEAN_OBJECT_NAME);
            cmsProxy = JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class);
            name = new ObjectName(AccordOperations.MBEAN_OBJECT_NAME);
            accordProxy = JMX.newMBeanProxy(mbeanServerConn, name, AccordOperationsMBean.class);
            name = new ObjectName(MessagingService.MBEAN_NAME);
            msProxy = JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class);
            name = new ObjectName(StreamManagerMBean.OBJECT_NAME);
            streamProxy = JMX.newMBeanProxy(mbeanServerConn, name, StreamManagerMBean.class);
            name = new ObjectName(CompactionManager.MBEAN_OBJECT_NAME);
            compactionProxy = JMX.newMBeanProxy(mbeanServerConn, name, CompactionManagerMBean.class);
            name = new ObjectName(FailureDetector.MBEAN_NAME);
            fdProxy = JMX.newMBeanProxy(mbeanServerConn, name, FailureDetectorMBean.class);
            name = new ObjectName(CacheService.MBEAN_NAME);
            cacheService = JMX.newMBeanProxy(mbeanServerConn, name, CacheServiceMBean.class);
            name = new ObjectName(StorageProxy.MBEAN_NAME);
            spProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageProxyMBean.class);
            name = new ObjectName(HintsService.MBEAN_NAME);
            hsProxy = JMX.newMBeanProxy(mbeanServerConn, name, HintsServiceMBean.class);
            name = new ObjectName(GCInspector.MBEAN_NAME);
            gcProxy = JMX.newMBeanProxy(mbeanServerConn, name, GCInspectorMXBean.class);
            name = new ObjectName(Gossiper.MBEAN_NAME);
            gossProxy = JMX.newMBeanProxy(mbeanServerConn, name, GossiperMBean.class);
            name = new ObjectName(BatchlogManager.MBEAN_NAME);
            bmProxy = JMX.newMBeanProxy(mbeanServerConn, name, BatchlogManagerMBean.class);
            name = new ObjectName(ActiveRepairServiceMBean.MBEAN_NAME);
            arsProxy = JMX.newMBeanProxy(mbeanServerConn, name, ActiveRepairServiceMBean.class);
            name = new ObjectName(AuditLogManager.MBEAN_NAME);
            almProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuditLogManagerMBean.class);
            name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME);
            ccProxy = JMX.newMBeanProxy(mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class);
            name = new ObjectName(AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME);
            jpcProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class);

            name = new ObjectName(AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME);
            npcProxy = JMX.newMBeanProxy(mbeanServerConn, name, NetworkPermissionsCacheMBean.class);

            name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME);
            pcProxy = JMX.newMBeanProxy(mbeanServerConn, name, PermissionsCacheMBean.class);

            name = new ObjectName(AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME);
            rcProxy = JMX.newMBeanProxy(mbeanServerConn, name, RolesCacheMBean.class);

            name = new ObjectName(CIDRPermissionsManager.MBEAN_NAME);
            cpbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRPermissionsManagerMBean.class);

            name = new ObjectName(CIDRGroupsMappingManager.MBEAN_NAME);
            cmbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class);

            name = new ObjectName(CIDRFilteringMetricsTable.MBEAN_NAME);
            cfmProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class);

            name = new ObjectName(AutoRepairService.MBEAN_NAME);
            autoRepairProxy = JMX.newMBeanProxy(mbeanServerConn, name, AutoRepairServiceMBean.class);

            name = new ObjectName(AsyncProfilerService.MBEAN_NAME);
            asyncProfilerProxy = JMX.newMBeanProxy(mbeanServerConn, name, AsyncProfilerMBean.class);

            name = new ObjectName(Guardrails.MBEAN_NAME);
            grProxy = JMX.newMBeanProxy(mbeanServerConn, name, GuardrailsMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(
                    "Invalid ObjectName? Please report this as a bug.", e);
        }

        memProxy = ManagementFactory.newPlatformMXBeanProxy(mbeanServerConn,
                ManagementFactory.MEMORY_MXBEAN_NAME, MemoryMXBean.class);
        runtimeProxy = ManagementFactory.newPlatformMXBeanProxy(
                mbeanServerConn, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class);
    }

    private RMIClientSocketFactory getRMIClientSocketFactory()
    {
        if (SSL_ENABLE.getBoolean())
            return new SslRMIClientSocketFactory();
        else
            return RMISocketFactory.getDefaultSocketFactory();
    }

    public void close() throws IOException
    {
        try
        {
            jmxc.close();
        }
        catch (ConnectException e)
        {
            // result of 'stopdaemon' command - i.e. if close() call fails, the daemon is shutdown
            System.out.println("Cassandra has shutdown.");
        }
    }

    public void setOutput(Output output)
    {
        this.output = output;
    }

    public Output output()
    {
        return output;
    }

    public int forceKeyspaceCleanup(int jobs, String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.forceKeyspaceCleanup(jobs, keyspaceName, tables);
    }

    public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTL, int jobs, String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTL, jobs, keyspaceName, tables);
    }

    public int verify(boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, boolean onlySai, boolean includeSai, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.verify(extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, onlySai, includeSai, keyspaceName, tableNames);
    }

    public int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, long maxSSTableTimestamp, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.upgradeSSTables(keyspaceName, excludeCurrentVersion, maxSSTableTimestamp, jobs, tableNames);
    }

    public int garbageCollect(String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.garbageCollect(tombstoneOption, jobs, keyspaceName, tableNames);
    }

    public int recompressSSTables(String keyspaceName, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        return ssProxy.recompressSSTables(keyspaceName, jobs, tableNames);
    }

    private void checkJobs(PrintStream out, int jobs)
    {
        int compactors = ssProxy.getConcurrentCompactors();
        if (jobs > compactors)
            out.println(String.format("jobs (%d) is bigger than configured concurrent_compactors (%d) on the host, using at most %d threads", jobs, compactors, compactors));
    }

    public void forceKeyspaceCleanup(PrintStream out, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        checkJobs(out, jobs);
        perform(out, keyspaceName,
                () -> forceKeyspaceCleanup(jobs, keyspaceName, tableNames),
                "cleaning up");
    }

    public void scrub(PrintStream out, boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTL, int jobs, String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException
    {
        checkJobs(out, jobs);
        perform(out, keyspaceName,
                () -> scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTL, jobs, keyspaceName, tables),
                "scrubbing");
    }

    public void verify(PrintStream out, boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, boolean onlySai, boolean includeSai, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        perform(out, keyspaceName,
                () -> verify(extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, onlySai, includeSai, keyspaceName, tableNames),
                "verifying");
    }

    public void recompressSSTables(PrintStream out, String keyspaceName, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        checkJobs(out, jobs);
        perform(out, keyspaceName,
                () -> recompressSSTables(keyspaceName, jobs, tableNames),
                "recompressing sstables");
    }

    public void upgradeSSTables(PrintStream out, String keyspaceName, boolean excludeCurrentVersion, long maxSSTableTimestamp, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        checkJobs(out, jobs);
        perform(out, keyspaceName,
                () -> upgradeSSTables(keyspaceName, excludeCurrentVersion, maxSSTableTimestamp, jobs, tableNames),
                "upgrading sstables");
    }

    private static interface Job
    {
        int perform() throws IOException, ExecutionException, InterruptedException;
    }

    private void perform(PrintStream out, String ks, Job job, String jobName) throws IOException, ExecutionException, InterruptedException
    {
        switch (job.perform())
        {
            case 1:
                out.printf("Aborted %s for at least one table in keyspace %s, check server logs for more information.\n",
                           jobName, ks);
                break;
            case 2:
                out.printf("Failed marking some sstables compacting in keyspace %s, check server logs for more information.\n", ks);
                throw new RuntimeException(String.format("Failed marking some sstables compacting in keyspace %s, check server logs for more information.\n", ks));
        }
    }

    public void garbageCollect(PrintStream out, String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        if (garbageCollect(tombstoneOption, jobs, keyspaceName, tableNames) != 0)
        {
            out.println("Aborted garbage collection for at least one table in keyspace " + keyspaceName + ", check server logs for more information.");
            throw new RuntimeException("Aborted garbage collection for at least one table in keyspace " + keyspaceName + ", check server logs for more information.");
        }
    }

    public void forceUserDefinedCompaction(String datafiles) throws IOException, ExecutionException, InterruptedException
    {
        compactionProxy.forceUserDefinedCompaction(datafiles);
    }

    public void forceKeyspaceCompaction(boolean splitOutput, int parallelism, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.forceKeyspaceCompaction(splitOutput, parallelism, keyspaceName, tableNames);
    }

    public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.forceKeyspaceCompaction(splitOutput, keyspaceName, tableNames);
    }

    public void relocateSSTables(int jobs, String keyspace, String[] cfnames) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.relocateSSTables(jobs, keyspace, cfnames);
    }

    /**
     * Forces major compaction of specified token range in a single keyspace.
     *
     * @param keyspaceName the name of the keyspace to be compacted
     * @param startToken the token at which the compaction range starts (inclusive)
     * @param endToken the token at which compaction range ends (inclusive)
     * @param tableNames the names of the tables to be compacted
     */
    public void forceKeyspaceCompactionForTokenRange(String keyspaceName, final String startToken, final String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.forceKeyspaceCompactionForTokenRange(keyspaceName, startToken, endToken, tableNames);
    }

    public void forceKeyspaceCompactionForPartitionKey(String keyspaceName, String partitionKey, String... tableNames) throws InterruptedException, ExecutionException, IOException
    {
        ssProxy.forceKeyspaceCompactionForPartitionKey(keyspaceName, partitionKey, tableNames);
    }

    public void forceCompactionKeysIgnoringGcGrace(String keyspaceName, String tableName, String... partitionKeysIgnoreGcGrace) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace);
    }

    public void forceKeyspaceFlush(String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
    {
        ssProxy.forceKeyspaceFlush(keyspaceName, tableNames);
    }

    public String getKeyspaceReplicationInfo(String keyspaceName)
    {
        return ssProxy.getKeyspaceReplicationInfo(keyspaceName);
    }

    public void repairAsync(final PrintStream out, final String keyspace, Map<String, String> options) throws IOException
    {
        startAndBlockOnAsyncRepairs(out, Collections.singleton(new RepairCmd(keyspace)
        {
            @Override
            public Integer start()
            {
                return ssProxy.repairAsync(keyspace, options);
            }
        }));
    }

    public void startAndBlockOnAsyncRepairs(final PrintStream out, Collection<RepairCmd> cmds) throws IOException
    {
        List<RepairRunner> runners = new ArrayList<>(cmds.size());
        for (RepairCmd cmd : cmds)
            runners.add(new RepairRunner(out, jmxc, ssProxy, cmd));

        try
        {
            runners.forEach(RepairRunner::start);

            for (RepairRunner runner : runners)
                runner.run();
        }
        catch (Exception e)
        {
            throw new IOException(e);
        }
        finally
        {
            try
            {
                runners.forEach(RepairRunner::close);
            }
            catch (Throwable e)
            {
                out.println("Exception occurred during clean-up. " + e);
            }
        }
    }

    public boolean handleScheduledSampling(String ks,
                                           String table,
                                           int capacity,
                                           int count,
                                           int durationMillis,
                                           int intervalMillis,
                                           List<String> samplers,
                                           boolean shouldStop) throws OpenDataException
    {
        return shouldStop ?
               ssProxy.stopSamplingPartitions(ks, table) :
               ssProxy.startSamplingPartitions(ks, table, durationMillis, intervalMillis, capacity, count, samplers);
    }

    public List<String> getSampleTasks()
    {
        return ssProxy.getSampleTasks();
    }

    public Map<String, List<CompositeData>> getPartitionSample(String ks, int capacity, int durationMillis, int count, List<String> samplers) throws OpenDataException
    {
        return ssProxy.samplePartitions(ks, durationMillis, capacity, count, samplers);
    }

    public Map<String, List<CompositeData>> getPartitionSample(String ks, String cf, int capacity, int durationMillis, int count, List<String> samplers) throws OpenDataException
    {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(ks, cf);
        for(String sampler : samplers)
        {
            cfsProxy.beginLocalSampling(sampler, capacity, durationMillis);
        }
        Uninterruptibles.sleepUninterruptibly(durationMillis, TimeUnit.MILLISECONDS);
        Map<String, List<CompositeData>> result = Maps.newHashMap();
        for(String sampler : samplers)
        {
            result.put(sampler, cfsProxy.finishLocalSampling(sampler, count));
        }
        return result;
    }

    public double getDroppableTombstoneRatio(String keyspace, String table) {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(keyspace, table);
        return cfsProxy.getDroppableTombstoneRatio();
    }

    public void invalidateCounterCache()
    {
        cacheService.invalidateCounterCache();
    }

    public void invalidateCredentialsCache()
    {
        ccProxy.invalidate();
    }

    public void invalidateCredentialsCache(String roleName)
    {
        ccProxy.invalidateCredentials(roleName);
    }

    public void invalidateJmxPermissionsCache()
    {
        jpcProxy.invalidate();
    }

    public void invalidateJmxPermissionsCache(String roleName)
    {
        jpcProxy.invalidatePermissions(roleName);
    }

    public void invalidateKeyCache()
    {
        cacheService.invalidateKeyCache();
    }

    public void invalidateNetworkPermissionsCache()
    {
        npcProxy.invalidate();
    }

    public void invalidateNetworkPermissionsCache(String roleName)
    {
        npcProxy.invalidateNetworkPermissions(roleName);
    }

    public boolean invalidateCidrPermissionsCache(String roleName)
    {
        return cpbProxy.invalidateCidrPermissionsCache(roleName);
    }

    public void reloadCidrGroupsCache()
    {
        cmbProxy.loadCidrGroupsCache();
    }

    public Set<String> listAvailableCidrGroups()
    {
        return cmbProxy.getAvailableCidrGroups();
    }

    public Set<String> listCidrsOfCidrGroup(String cidrGroup)
    {
        return cmbProxy.getCidrsOfCidrGroupAsStrings(cidrGroup);
    }

    public void updateCidrGroup(String cidrGroupName, List<String> cidrs)
    {
        cmbProxy.updateCidrGroup(cidrGroupName, cidrs);
    }

    public void dropCidrGroup(String cidrGroupName)
    {
        cmbProxy.dropCidrGroup(cidrGroupName);
    }

    public Set<String> getCidrGroupsOfIp(String ipStr)
    {
        return cmbProxy.getCidrGroupsOfIP(ipStr);
    }

    public void invalidatePermissionsCache()
    {
        pcProxy.invalidate();
    }

    public void invalidatePermissionsCache(String roleName, String resourceName)
    {
        pcProxy.invalidatePermissions(roleName, resourceName);
    }

    public void invalidateRolesCache()
    {
        rcProxy.invalidate();
    }

    public void invalidateRolesCache(String roleName)
    {
        rcProxy.invalidateRoles(roleName);
    }

    public void invalidateRowCache()
    {
        cacheService.invalidateRowCache();
    }

    public AuthCacheMBean getAuthCacheMBean(String cacheName)
    {
        switch (cacheName)
        {
            case PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME:
                return ccProxy;
            case AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME:
                return jpcProxy;
            case NetworkPermissionsCacheMBean.CACHE_NAME:
                return npcProxy;
            case PermissionsCacheMBean.CACHE_NAME:
                return pcProxy;
            case RolesCacheMBean.CACHE_NAME:
                return rcProxy;
            default:
                throw new IllegalArgumentException("Unknown cache name: " + cacheName);
        }
    }

    public void drain() throws IOException, InterruptedException, ExecutionException
    {
        ssProxy.drain();
    }

    public Map<String, String> getTokenToEndpointMap(boolean withPort)
    {
        return withPort ? ssProxy.getTokenToEndpointWithPortMap() : ssProxy.getTokenToEndpointMap();
    }

    public List<String> getLiveNodes(boolean withPort)
    {
        return withPort ? ssProxy.getLiveNodesWithPort() : ssProxy.getLiveNodes();
    }

    public List<String> getJoiningNodes(boolean withPort)
    {
        return withPort ? ssProxy.getJoiningNodesWithPort() : ssProxy.getJoiningNodes();
    }

    public List<String> getLeavingNodes(boolean withPort)
    {
        return withPort ? ssProxy.getLeavingNodesWithPort() : ssProxy.getLeavingNodes();
    }

    public List<String> getMovingNodes(boolean withPort)
    {
        return withPort ? ssProxy.getMovingNodesWithPort() : ssProxy.getMovingNodes();
    }

    public List<String> getUnreachableNodes(boolean withPort)
    {
        return withPort ? ssProxy.getUnreachableNodesWithPort() : ssProxy.getUnreachableNodes();
    }

    public Map<String, String> getLoadMap(boolean withPort)
    {
        return withPort ? ssProxy.getLoadMapWithPort() : ssProxy.getLoadMap();
    }

    public Map<InetAddress, Float> getOwnership()
    {
        return ssProxy.getOwnership();
    }

    public Map<String, Float> getOwnershipWithPort()
    {
        return ssProxy.getOwnershipWithPort();
    }

    public Map<InetAddress, Float> effectiveOwnership(String keyspace) throws IllegalStateException
    {
        return ssProxy.effectiveOwnership(keyspace);
    }

    public Map<String, Float> effectiveOwnershipWithPort(String keyspace) throws IllegalStateException
    {
        return ssProxy.effectiveOwnershipWithPort(keyspace);
    }

    public MBeanServerConnection getMbeanServerConn()
    {
        return mbeanServerConn;
    }

    public CacheServiceMBean getCacheServiceMBean()
    {
        String cachePath = "org.apache.cassandra.db:type=Caches";

        try
        {
            return JMX.newMBeanProxy(mbeanServerConn, new ObjectName(cachePath), CacheServiceMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public double[] getAndResetGCStats()
    {
        return gcProxy.getAndResetStats();
    }

    public Iterator<Map.Entry<String, ColumnFamilyStoreMBean>> getColumnFamilyStoreMBeanProxies()
    {
        try
        {
            return new ColumnFamilyStoreMBeanIterator(mbeanServerConn);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException("Invalid ObjectName? Please report this as a bug.", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("Could not retrieve list of stat mbeans.", e);
        }
    }

    public CompactionManagerMBean getCompactionManagerProxy()
    {
      return compactionProxy;
    }

    public List<String> getTokens()
    {
        return ssProxy.getTokens();
    }

    public List<String> getTokens(String endpoint)
    {
        try
        {
            return ssProxy.getTokens(endpoint);
        }
        catch (UnknownHostException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Map<String, String> getHostIdToEndpointWithPort()
    {
        return ssProxy.getHostIdToEndpointWithPort();
    }

    public String getLocalHostId()
    {
        return ssProxy.getLocalHostId();
    }

    public Map<String, String> getHostIdMap(boolean withPort)
    {
        return withPort ? ssProxy.getEndpointWithPortToHostId() : ssProxy.getEndpointToHostId();
    }

    public String getLoadString()
    {
        return ssProxy.getLoadString();
    }

    public String getUncompressedLoadString()
    {
        return ssProxy.getUncompressedLoadString();
    }

    public String getReleaseVersion()
    {
        return ssProxy.getReleaseVersion();
    }

    public String getGitSHA()
    {
        return ssProxy.getGitSHA();
    }

    public String getBuildDate()
    {
        return ssProxy.getBuildDate();
    }

    public int getCurrentGenerationNumber()
    {
        return ssProxy.getCurrentGenerationNumber();
    }

    public long getUptime()
    {
        return runtimeProxy.getUptime();
    }

    public MemoryUsage getHeapMemoryUsage()
    {
        return memProxy.getHeapMemoryUsage();
    }

    public long getSnapshotLinksPerSecond()
    {
        return snapshotProxy.getSnapshotLinksPerSecond();
    }

    public void setSnapshotLinksPerSecond(long throttle)
    {
        snapshotProxy.setSnapshotLinksPerSecond(throttle);
    }

    /**
     * Take a snapshot of all the keyspaces, optionally specifying only a specific column family.
     *
     * @param snapshotName the name of the snapshot.
     * @param table the table to snapshot or all on null
     * @param options Options (skipFlush for now)
     * @param keyspaces the keyspaces to snapshot
     */
    public void takeSnapshot(String snapshotName, String table, Map<String, String> options, String... keyspaces) throws IOException
    {
        if (table != null)
        {
            if (keyspaces.length != 1)
            {
                throw new IOException("When specifying the table for a snapshot, you must specify one and only one keyspace");
            }

            snapshotProxy.takeSnapshot(snapshotName, options, keyspaces[0] + "." + table);
        }
        else
            snapshotProxy.takeSnapshot(snapshotName, options, keyspaces);
    }

    /**
     * Take a snapshot of all column family from different keyspaces.
     *
     * @param snapshotName
     *            the name of the snapshot.
     * @param options
     *            Options (skipFlush for now)
     * @param tableList
     *            list of columnfamily from different keyspace in the form of ks1.cf1 ks2.cf2
     */
    public void takeMultipleTableSnapshot(String snapshotName, Map<String, String> options, String... tableList)
            throws IOException
    {
        if (null != tableList && tableList.length != 0)
        {
            snapshotProxy.takeSnapshot(snapshotName, options, tableList);
        }
        else
        {
            throw new IOException("The column family list for a snapshot should not be empty or null");
        }
    }

    /**
     * Remove all the existing snapshots of given tag for provided keyspaces.
     * When no keyspaces are specified, take all keyspaces into account. When tag is not specified (null or empty string),
     * take all tags into account.
     *
     * @param tag tag of snapshot to clear
     * @param keyspaces keyspaces to clear snapshots for
     */
    /** @deprecated See CASSANDRA-16860 */
    @Deprecated(since = "5.0")
    public void clearSnapshot(String tag, String... keyspaces) throws IOException
    {
        clearSnapshot(Collections.emptyMap(), tag, keyspaces);
    }

    /**
     * Remove all the existing snapshots of given tag for provided keyspaces.
     * When no keyspaces are specified, take all keyspaces into account. When tag is not specified (null or empty string),
     * take all tags into account.
     *
     * @param options options to supply for snapshot clearing
     * @param tag tag of snapshot to clear
     * @param keyspaces keyspaces to clear snapshots for
     */
    public void clearSnapshot(Map<String, Object> options, String tag, String... keyspaces) throws IOException
    {
        snapshotProxy.clearSnapshot(tag, options, keyspaces);
    }

    /**
     * Gets all snapshots' details.
     *
     * @param options options to use upon listing of snapshots
     * @return details of snapshots
     */
    /** @deprecated See CASSANDRA-18111 */
    @Deprecated(since = "5.1")
    public Map<String, TabularData> getSnapshotDetails(Map<String, String> options)
    {
        return snapshotProxy.listSnapshots(options);
    }

    /** @deprecated See CASSANDRA-16789 */
    @Deprecated(since = "4.1")
    public Map<String, TabularData> getSnapshotDetails()
    {
        return getSnapshotDetails(ImmutableMap.of());
    }

    public long trueSnapshotsSize()
    {
        return snapshotProxy.getTrueSnapshotSize();
    }

    public boolean isJoined()
    {
        return ssProxy.isJoined();
    }

    public boolean isDrained()
    {
        return ssProxy.isDrained();
    }

    public boolean isDraining()
    {
        return ssProxy.isDraining();
    }

    public boolean isBootstrapMode()
    {
        return ssProxy.isBootstrapMode();
    }

    public void joinRing() throws IOException
    {
        ssProxy.joinRing();
    }

    public void decommission(boolean force) throws InterruptedException
    {
        ssProxy.decommission(force);
    }

    public void abortDecommission(String nodeId)
    {
        ssProxy.abortDecommission(nodeId);
    }

    public void move(String newToken) throws IOException
    {
        ssProxy.move(newToken);
    }

    public void resumeMove()
    {
        ssProxy.resumeMove();
    }

    public void abortMove(String nodeId)
    {
        ssProxy.abortMove(nodeId);
    }

    public void removeNode(String token)
    {
        removeNode(token, false);
    }

    public void removeNode(String token, boolean force)
    {
        ssProxy.removeNode(token, force);
    }

    public void abortRemoveNode(String nodeId)
    {
        ssProxy.abortRemoveNode(nodeId);
    }

    public String getRemovalStatus(boolean withPort)
    {
        return withPort ? ssProxy.getRemovalStatusWithPort() : ssProxy.getRemovalStatus();
    }

    public void forceRemoveCompletion()
    {
        ssProxy.forceRemoveCompletion();
    }

    public void assassinateEndpoint(String address) throws UnknownHostException
    {
        ssProxy.assassinateEndpoint(address);
    }

    public List<String> reloadSeeds()
    {
        return gossProxy.reloadSeeds();
    }

    public List<String> getSeeds()
    {
        return gossProxy.getSeeds();
    }

    /**
     * Set the compaction threshold
     *
     * @param minimumCompactionThreshold minimum compaction threshold
     * @param maximumCompactionThreshold maximum compaction threshold
     */
    public void setCompactionThreshold(String ks, String cf, int minimumCompactionThreshold, int maximumCompactionThreshold)
    {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(ks, cf);
        cfsProxy.setCompactionThresholds(minimumCompactionThreshold, maximumCompactionThreshold);
    }

    public void disableAutoCompaction(String ks, String ... tables) throws IOException
    {
        ssProxy.disableAutoCompaction(ks, tables);
    }

    public void enableAutoCompaction(String ks, String ... tableNames) throws IOException
    {
        ssProxy.enableAutoCompaction(ks, tableNames);
    }

    public Map<String, Boolean> getAutoCompactionDisabled(String ks, String ... tableNames) throws IOException
    {
        return ssProxy.getAutoCompactionStatus(ks, tableNames);
    }

    public void setIncrementalBackupsEnabled(boolean enabled)
    {
        ssProxy.setIncrementalBackupsEnabled(enabled);
    }

    public boolean isIncrementalBackupsEnabled()
    {
        return ssProxy.isIncrementalBackupsEnabled();
    }

    public void setCacheCapacities(int keyCacheCapacity, int rowCacheCapacity, int counterCacheCapacity)
    {
        CacheServiceMBean cacheMBean = getCacheServiceMBean();
        cacheMBean.setKeyCacheCapacityInMB(keyCacheCapacity);
        cacheMBean.setRowCacheCapacityInMB(rowCacheCapacity);
        cacheMBean.setCounterCacheCapacityInMB(counterCacheCapacity);
    }

    public void setCacheKeysToSave(int keyCacheKeysToSave, int rowCacheKeysToSave, int counterCacheKeysToSave)
    {
        CacheServiceMBean cacheMBean = getCacheServiceMBean();
        cacheMBean.setKeyCacheKeysToSave(keyCacheKeysToSave);
        cacheMBean.setRowCacheKeysToSave(rowCacheKeysToSave);
        cacheMBean.setCounterCacheKeysToSave(counterCacheKeysToSave);
    }

    public void setHintedHandoffThrottleInKB(int throttleInKB)
    {
        ssProxy.setHintedHandoffThrottleInKB(throttleInKB);
    }

    public List<String> getEndpointsWithPort(String keyspace, String cf, String key)
    {
        return ssProxy.getNaturalEndpointsWithPort(keyspace, cf, key);
    }

    public List<InetAddress> getEndpoints(String keyspace, String cf, String key)
    {
        return ssProxy.getNaturalEndpoints(keyspace, cf, key);
    }

    public List<String> getSSTables(String keyspace, String cf, String key, boolean hexFormat)
    {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(keyspace, cf);
        return cfsProxy.getSSTablesForKey(key, hexFormat);
    }

    public Map<Integer, Set<String>> getSSTablesWithLevel(String keyspace, String cf, String key, boolean hexFormat)
    {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(keyspace, cf);
        return cfsProxy.getSSTablesForKeyWithLevel(key, hexFormat);
    }

    public boolean isLeveledCompaction(String keyspace, String cf)
    {
        ColumnFamilyStoreMBean cfsProxy = getCfsProxy(keyspace, cf);
        return cfsProxy.isLeveledCompaction();
    }

    public Set<StreamState> getStreamStatus()
    {
        return Sets.newHashSet(Iterables.transform(streamProxy.getCurrentStreams(), new Function<CompositeData, StreamState>()
        {
            public StreamState apply(CompositeData input)
            {
                return StreamStateCompositeData.fromCompositeData(input);
            }
        }));
    }

    public String getOperationMode()
    {
        return ssProxy.getOperationMode();
    }

    public boolean isStarting()
    {
        return ssProxy.isStarting();
    }

    public void truncate(String keyspaceName, String tableName)
    {
        try
        {
            ssProxy.truncate(keyspaceName, tableName);
        }
        catch (TimeoutException e)
        {
            throw new RuntimeException("Error while executing truncate", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("Error while executing truncate", e);
        }
    }

    public EndpointSnitchInfoMBean getEndpointSnitchInfoProxy()
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn, new ObjectName("org.apache.cassandra.db:type=EndpointSnitchInfo"), EndpointSnitchInfoMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public DynamicEndpointSnitchMBean getDynamicEndpointSnitchInfoProxy()
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn, new ObjectName("org.apache.cassandra.db:type=DynamicEndpointSnitch"), DynamicEndpointSnitchMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public LocationInfoMBean getLocationInfoProxy()
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn, new ObjectName("org.apache.cassandra.db:type=LocationInfo"), LocationInfoMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public ColumnFamilyStoreMBean getCfsProxy(String ks, String cf)
    {
        ColumnFamilyStoreMBean cfsProxy = null;
        try
        {
            String type = cf.contains(".") ? "IndexColumnFamilies" : "ColumnFamilies";
            Set<ObjectName> beans = mbeanServerConn.queryNames(
                    new ObjectName("org.apache.cassandra.db:type=*" + type +",keyspace=" + ks + ",columnfamily=" + cf), null);

            if (beans.isEmpty())
                throw new MalformedObjectNameException("couldn't find that bean");
            assert beans.size() == 1;
            for (ObjectName bean : beans)
                cfsProxy = JMX.newMBeanProxy(mbeanServerConn, bean, ColumnFamilyStoreMBean.class);
        }
        catch (MalformedObjectNameException mone)
        {
            System.err.println("ColumnFamilyStore for " + ks + "/" + cf + " not found.");
            System.exit(1);
        }
        catch (IOException e)
        {
            System.err.println("ColumnFamilyStore for " + ks + "/" + cf + " not found: " + e);
            System.exit(1);
        }

        return cfsProxy;
    }

    public StorageProxyMBean getSpProxy()
    {
        return spProxy;
    }

    public StorageServiceMBean getStorageService()
    {
        return ssProxy;
    }

    public CMSOperationsMBean getCMSOperationsProxy()
    {
        return cmsProxy;
    }

    public AccordOperationsMBean getAccordOperationsProxy()
    {
        return accordProxy;
    }

    public AsyncProfilerMBean getAsyncProfilerProxy()
    {
        return asyncProfilerProxy;
    }

    public GossiperMBean getGossProxy()
    {
        return gossProxy;
    }

    public String getEndpoint()
    {
        Map<String, String> hostIdToEndpoint = ssProxy.getHostIdToEndpoint();
        return hostIdToEndpoint.get(ssProxy.getLocalHostId());
    }

    public String getDataCenter()
    {
        return getEndpointSnitchInfoProxy().getDatacenter();
    }

    public String getRack()
    {
        return getEndpointSnitchInfoProxy().getRack();
    }

    public List<String> getKeyspaces()
    {
        return ssProxy.getKeyspaces();
    }

    public List<String> getNonSystemKeyspaces()
    {
        return ssProxy.getNonSystemKeyspaces();
    }

    public List<String> getNonLocalStrategyKeyspaces()
    {
        return ssProxy.getNonLocalStrategyKeyspaces();
    }

    public List<String> getAccordManagedKeyspaces()
    {
        return ssProxy.getAccordManagedKeyspaces();
    }

    public List<String> getAccordManagedTables()
    {
        return ssProxy.getAccordManagedTables();
    }

    public String getClusterName()
    {
        return ssProxy.getClusterName();
    }

    public String getPartitioner()
    {
        return ssProxy.getPartitionerName();
    }

    public void disableHintedHandoff()
    {
        spProxy.setHintedHandoffEnabled(false);
    }

    public void enableHintedHandoff()
    {
        spProxy.setHintedHandoffEnabled(true);
    }

    public boolean isHandoffEnabled()
    {
        return spProxy.getHintedHandoffEnabled();
    }

    public void enableHintsForDC(String dc)
    {
        spProxy.enableHintsForDC(dc);
    }

    public void disableHintsForDC(String dc)
    {
        spProxy.disableHintsForDC(dc);
    }

    public Set<String> getHintedHandoffDisabledDCs()
    {
        return spProxy.getHintedHandoffDisabledDCs();
    }

    public Map<String, String> getViewBuildStatuses(String keyspace, String view)
    {
        return ssProxy.getViewBuildStatuses(keyspace, view);
    }

    public void pauseHintsDelivery()
    {
        hsProxy.pauseDispatch();
    }

    public void resumeHintsDelivery()
    {
        hsProxy.resumeDispatch();
    }

    public void truncateHints(final String host)
    {
        hsProxy.deleteAllHintsForEndpoint(host);
    }

    public void truncateHints()
    {
        hsProxy.deleteAllHints();
    }

    public List<Map<String, String>> listPendingHints()
    {
        return hsProxy.getPendingHints();
    }

    public void refreshSizeEstimates()
    {
        try
        {
            ssProxy.refreshSizeEstimates();
        }
        catch (ExecutionException e)
        {
            throw new RuntimeException("Error while refreshing system.size_estimates", e);
        }
    }

    public void stopNativeTransport(boolean force)
    {
        ssProxy.stopNativeTransport(force);
    }

    public void startNativeTransport()
    {
        ssProxy.startNativeTransport();
    }

    public boolean isNativeTransportRunning()
    {
        return ssProxy.isNativeTransportRunning();
    }

    public void stopGossiping()
    {
        ssProxy.stopGossiping();
    }

    public void startGossiping()
    {
        ssProxy.startGossiping();
    }

    public boolean isGossipRunning()
    {
        return ssProxy.isGossipRunning();
    }

    public void stopCassandraDaemon()
    {
        ssProxy.stopDaemon();
    }

    public boolean isInitialized()
    {
        return ssProxy.isInitialized();
    }

    public void setColumnIndexSize(int columnIndexSizeInKiB)
    {
        ssProxy.setColumnIndexSize(columnIndexSizeInKiB);
    }

    public int getColumnIndexSizeInKB()
    {
        return ssProxy.getColumnIndexSizeInKiB();
    }

    public void setCompactionThroughput(int value)
    {
        ssProxy.setCompactionThroughputMbPerSec(value);
    }

    /** @deprecated See CASSANDRA-17225 */
    @Deprecated(since = "4.1")
    public int getCompactionThroughput()
    {
        return ssProxy.getCompactionThroughputMbPerSec();
    }

    public double getCompactionThroughputMebibytesAsDouble()
    {
        return ssProxy.getCompactionThroughtputMibPerSecAsDouble();
    }

    public long getCompactionThroughputBytes()
    {
        return ssProxy.getCompactionThroughtputBytesPerSec();
    }

    public Map<String, String> getCurrentCompactionThroughputMiBPerSec()
    {
        return ssProxy.getCurrentCompactionThroughputMebibytesPerSec();
    }

    public void setBatchlogReplayThrottle(int value)
    {
        ssProxy.setBatchlogReplayThrottleInKB(value);
    }

    public int getBatchlogReplayThrottle()
    {
        return ssProxy.getBatchlogReplayThrottleInKB();
    }

    public void setConcurrentCompactors(int value)
    {
        ssProxy.setConcurrentCompactors(value);
    }

    public int getConcurrentCompactors()
    {
        return ssProxy.getConcurrentCompactors();
    }

    public void setConcurrentViewBuilders(int value)
    {
        ssProxy.setConcurrentViewBuilders(value);
    }

    public int getConcurrentViewBuilders()
    {
        return ssProxy.getConcurrentViewBuilders();
    }

    public void setMaxHintWindow(int value)
    {
        spProxy.setMaxHintWindow(value);
    }

    public int getMaxHintWindow()
    {
        return spProxy.getMaxHintWindow();
    }

    public long getTimeout(String type)
    {
        switch (type)
        {
            case "misc":
                return ssProxy.getRpcTimeout();
            case "read":
                return ssProxy.getReadRpcTimeout();
            case "range":
                return ssProxy.getRangeRpcTimeout();
            case "write":
                return ssProxy.getWriteRpcTimeout();
            case "counterwrite":
                return ssProxy.getCounterWriteRpcTimeout();
            case "cascontention":
                return ssProxy.getCasContentionTimeout();
            case "truncate":
                return ssProxy.getTruncateRpcTimeout();
            case "internodeconnect":
                return ssProxy.getInternodeTcpConnectTimeoutInMS();
            case "internodeuser":
                return ssProxy.getInternodeTcpUserTimeoutInMS();
            case "internodestreaminguser":
                return ssProxy.getInternodeStreamingTcpUserTimeoutInMS();
            default:
                throw new RuntimeException("Timeout type requires one of (" + GetTimeout.TIMEOUT_TYPES + ")");
        }
    }

    /** @deprecated See CASSANDRA-17225 */
    @Deprecated(since = "4.1")
    public int getStreamThroughput()
    {
        return ssProxy.getStreamThroughputMbitPerSec();
    }

    public double getStreamThroughputAsDouble()
    {
        return ssProxy.getStreamThroughputMbitPerSecAsDouble();
    }

    /** @deprecated See CASSANDRA-17225 */
    @Deprecated(since = "4.1")
    public int getInterDCStreamThroughput()
    {
        return ssProxy.getInterDCStreamThroughputMbitPerSec();
    }

    public double getInterDCStreamThroughputAsDouble()
    {
        return ssProxy.getInterDCStreamThroughputMbitPerSecAsDouble();
    }

    public double getStreamThroughputMibAsDouble()
    {
        return ssProxy.getStreamThroughputMebibytesPerSecAsDouble();
    }

    public double getInterDCStreamThroughputMibAsDouble()
    {
        return ssProxy.getInterDCStreamThroughputMebibytesPerSecAsDouble();
    }

    public double getEntireSSTableStreamThroughput()
    {
        return ssProxy.getEntireSSTableStreamThroughputMebibytesPerSecAsDouble();
    }

    public double getEntireSSTableInterDCStreamThroughput()
    {
        return ssProxy.getEntireSSTableInterDCStreamThroughputMebibytesPerSecAsDouble();
    }

    public double getTraceProbability()
    {
        return ssProxy.getTraceProbability();
    }

    public int getExceptionCount()
    {
        return (int)StorageMetrics.uncaughtExceptions.getCount();
    }

    public Map<String, Integer> getDroppedMessages()
    {
        return msProxy.getDroppedMessages();
    }

    /** @deprecated See CASSANDRA-6719 */
    @Deprecated(since = "4.0")
    public void loadNewSSTables(String ksName, String cfName)
    {
        ssProxy.loadNewSSTables(ksName, cfName);
    }

    public List<String> importNewSSTables(String ksName, String cfName, Set<String> srcPaths, boolean resetLevel,
                                          boolean clearRepaired, boolean verifySSTables, boolean verifyTokens,
                                          boolean invalidateCaches, boolean extendedVerify, boolean copyData,
                                          boolean failOnMissingIndex, boolean validateIndexChecksum)
    {
        return getCfsProxy(ksName, cfName).importNewSSTables(srcPaths, resetLevel, clearRepaired,
                                                             verifySSTables, verifyTokens, invalidateCaches,
                                                             extendedVerify, copyData, failOnMissingIndex,
                                                             validateIndexChecksum);
    }

    public void rebuildIndex(String ksName, String cfName, String... idxNames)
    {
        ssProxy.rebuildSecondaryIndex(ksName, cfName, idxNames);
    }

    public Map<String, String> getSimpleStatesWithPort()
    {
        return fdProxy.getSimpleStatesWithPort();
    }

    public String getGossipInfo(boolean withPort)
    {
        return getGossipInfo(withPort, false);
    }

    public String getGossipInfo(boolean withPort, boolean resolveIp)
    {
        if (resolveIp)
            return withPort ? fdProxy.getAllEndpointStatesWithPortAndResolveIp() : fdProxy.getAllEndpointStatesWithResolveIp();
        else
            return withPort ? fdProxy.getAllEndpointStatesWithPort() : fdProxy.getAllEndpointStates();
    }

    public void stop(String string)
    {
        compactionProxy.stopCompaction(string);
    }

    public void setTimeout(String type, long value)
    {
        if (value < 0)
            throw new RuntimeException("timeout must be non-negative");

        switch (type)
        {
            case "misc":
                ssProxy.setRpcTimeout(value);
                break;
            case "read":
                ssProxy.setReadRpcTimeout(value);
                break;
            case "range":
                ssProxy.setRangeRpcTimeout(value);
                break;
            case "write":
                ssProxy.setWriteRpcTimeout(value);
                break;
            case "counterwrite":
                ssProxy.setCounterWriteRpcTimeout(value);
                break;
            case "cascontention":
                ssProxy.setCasContentionTimeout(value);
                break;
            case "truncate":
                ssProxy.setTruncateRpcTimeout(value);
                break;
            case "internodeconnect":
                ssProxy.setInternodeTcpConnectTimeoutInMS((int) value);
                break;
            case "internodeuser":
                ssProxy.setInternodeTcpUserTimeoutInMS((int) value);
                break;
            case "internodestreaminguser":
                ssProxy.setInternodeStreamingTcpUserTimeoutInMS((int) value);
                break;
            default:
                throw new RuntimeException("Timeout type requires one of (" + GetTimeout.TIMEOUT_TYPES + ")");
        }
    }

    public void stopById(String compactionId)
    {
        compactionProxy.stopCompactionById(compactionId);
    }

    public void setStreamThroughput(int value)
    {
        ssProxy.setStreamThroughputMbitPerSec(value);
    }

    public void setStreamThroughputMiB(int value)
    {
        ssProxy.setStreamThroughputMebibytesPerSec(value);
    }

    public void setInterDCStreamThroughput(int value)
    {
        ssProxy.setInterDCStreamThroughputMbitPerSec(value);
    }

    public void setInterDCStreamThroughputMiB(int value)
    {
        ssProxy.setInterDCStreamThroughputMebibytesPerSec(value);
    }

    public void setEntireSSTableStreamThroughput(int value)
    {
        ssProxy.setEntireSSTableStreamThroughputMebibytesPerSec(value);
    }

    public void setEntireSSTableInterDCStreamThroughput(int value)
    {
        ssProxy.setEntireSSTableInterDCStreamThroughputMebibytesPerSec(value);
    }

    public void setTraceProbability(double value)
    {
        ssProxy.setTraceProbability(value);
    }

    public String getSchemaVersion()
    {
        return ssProxy.getSchemaVersion();
    }

    public List<String> describeRing(String keyspaceName, boolean withPort) throws IOException
    {
        return withPort ? ssProxy.describeRingWithPortJMX(keyspaceName) : ssProxy.describeRingJMX(keyspaceName);
    }

    public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources, boolean excludeLocalDatacenterNodes)
    {
        ssProxy.rebuild(sourceDc, keyspace, tokens, specificSources, excludeLocalDatacenterNodes);
    }

    public List<String> sampleKeyRange()
    {
        return ssProxy.sampleKeyRange();
    }

    public void resetLocalSchema() throws IOException
    {
        ssProxy.resetLocalSchema();
    }

    public void reloadLocalSchema()
    {
        ssProxy.reloadLocalSchema();
    }

    public long getReadRepairAttempted()
    {
        return spProxy.getReadRepairAttempted();
    }

    public long getReadRepairRepairedBlocking()
    {
        return spProxy.getReadRepairRepairedBlocking();
    }

    public long getReadRepairRepairedBackground()
    {
        return spProxy.getReadRepairRepairedBackground();
    }

    // JMX getters for the o.a.c.metrics API below.
    /**
     * Retrieve cache metrics based on the cache type (KeyCache, RowCache, or CounterCache)
     * @param cacheType KeyCach, RowCache, or CounterCache
     * @param metricName Capacity, Entries, HitRate, Size, Requests or Hits.
     */
    public Object getCacheMetric(String cacheType, String metricName)
    {
        try
        {
            switch(metricName)
            {
                case "Capacity":
                case "Entries":
                case "HitRate":
                case "Size":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=" + metricName),
                            CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                case "Requests":
                case "Hits":
                case "Misses":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=" + metricName),
                            CassandraMetricsRegistry.JmxMeterMBean.class).getCount();
                case "MissLatency":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=" + metricName),
                            CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
                case "MissLatencyUnit":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=MissLatency"),
                            CassandraMetricsRegistry.JmxTimerMBean.class).getDurationUnit();
                default:
                    throw new RuntimeException("Unknown Cache metric name " + metricName);

            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieve buffer pool metrics based on the buffer pool type
     * @param poolType networking chunk-cache
     * @param metricName UsedSize Size
     * @return
     */
    public Object getBufferPoolMetric(String poolType, String metricName)
    {
        try
        {
            switch (metricName)
            {
                case "UsedSize":
                case "OverflowSize":
                case "Capacity":
                case "Size":
                    return JMX.newMBeanProxy(mbeanServerConn,
                           new ObjectName("org.apache.cassandra.metrics:type=BufferPool,scope=" + poolType + ",name=" + metricName),
                           CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                case "Hits":
                case "Misses":
                    return JMX.newMBeanProxy(mbeanServerConn,
                    new ObjectName("org.apache.cassandra.metrics:type=BufferPool,scope=" + poolType + ",name=" + metricName),
                    CassandraMetricsRegistry.JmxMeterMBean.class).getCount();
                default:
                    throw new RuntimeException("Unknown BufferPool metric name " + metricName);
            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    private static Multimap<String, String> getJmxThreadPools(MBeanServerConnection mbeanServerConn)
    {
        try
        {
            Multimap<String, String> threadPools = HashMultimap.create();

            Set<ObjectName> threadPoolObjectNames = mbeanServerConn.queryNames(
                    new ObjectName("org.apache.cassandra.metrics:type=ThreadPools,*"),
                    null);

            for (ObjectName oName : threadPoolObjectNames)
            {
                threadPools.put(oName.getKeyProperty("path"), oName.getKeyProperty("scope"));
            }

            return threadPools;
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException("Bad query to JMX server: ", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("Error getting threadpool names from JMX", e);
        }
    }

    public Object getThreadPoolMetric(String pathName, String poolName, String metricName)
    {
      String name = String.format("org.apache.cassandra.metrics:type=ThreadPools,path=%s,scope=%s,name=%s",
              pathName, poolName, metricName);

      try
      {
          ObjectName oName = new ObjectName(name);
          if (!mbeanServerConn.isRegistered(oName))
          {
              return "N/A";
          }

          switch (metricName)
          {
              case ThreadPoolMetrics.ACTIVE_TASKS:
              case ThreadPoolMetrics.PENDING_TASKS:
              case ThreadPoolMetrics.COMPLETED_TASKS:
              case ThreadPoolMetrics.CORE_POOL_SIZE:
              case ThreadPoolMetrics.MAX_POOL_SIZE:
              case ThreadPoolMetrics.MAX_TASKS_QUEUED:
                  return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
              case ThreadPoolMetrics.TOTAL_BLOCKED_TASKS:
              case ThreadPoolMetrics.CURRENTLY_BLOCKED_TASKS:
                  return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
              default:
                  throw new AssertionError("Unknown ThreadPools metric name " + metricName);
          }
      }
      catch (Exception e)
      {
          throw new RuntimeException("Error reading: " + name, e);
      }
    }

    public Object getSaiMetric(String ks, String cf, String metricName)
    {
        try
        {
            String scope = getSaiMetricScope(metricName);
            String objectNameStr = String.format("org.apache.cassandra.metrics:type=StorageAttachedIndex,keyspace=%s,table=%s,scope=%s,name=%s",ks, cf, scope, metricName);
            ObjectName oName = new ObjectName(objectNameStr);

            Set<ObjectName> matchingMBeans = mbeanServerConn.queryNames(oName, null);
            if (matchingMBeans.isEmpty())
                return null;

            return getSaiMetricValue(metricName, oName);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException("Invalid ObjectName format: " + e.getMessage(), e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("Error accessing MBean server: " + e.getMessage(), e);
        }
    }

    private Object getSaiMetricValue(String metricName, ObjectName oName) throws IOException
    {
        switch (metricName)
        {
            case "QueryLatency":
                return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxTimerMBean.class);
            case "PostFilteringReadLatency":
            case "SSTableIndexesHit":
            case "IndexSegmentsHit":
            case "RowsFiltered":
                return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxHistogramMBean.class);
            case "DiskUsedBytes":
            case "TotalIndexCount":
            case "TotalQueryableIndexCount":
                return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
            case "TotalQueryTimeouts":
                return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
            default:
                throw new IllegalArgumentException("Unknown metric name: " + metricName);
        }
    }

    private String getSaiMetricScope(String metricName)
    {
        switch (metricName)
        {
            case "QueryLatency":
            case "SSTableIndexesHit":
            case "IndexSegmentsHit":
            case "RowsFiltered":
                return TableQueryMetrics.PerQueryMetrics.PER_QUERY_METRICS_TYPE;
            case "PostFilteringReadLatency":
            case "TotalQueryTimeouts":
                return TableQueryMetrics.TABLE_QUERY_METRIC_TYPE;
            case "DiskUsedBytes":
                return IndexGroupMetrics.INDEX_GROUP_METRICS_TYPE;
            case "TotalIndexCount":
            case "TotalQueryableIndexCount":
                return TableStateMetrics.TABLE_STATE_METRIC_TYPE;
            default:
                throw new IllegalArgumentException("Unknown metric name: " + metricName);
        }
    }

        /**
     * Retrieve threadpool paths and names for threadpools with metrics.
     * @return Multimap from path (internal, request, etc.) to name
     */
    public Multimap<String, String> getThreadPools()
    {
        return getJmxThreadPools(mbeanServerConn);
    }

    public int getNumberOfTables()
    {
        return spProxy.getNumberOfTables();
    }

    /**
     * Retrieve ColumnFamily metrics
     * @param ks Keyspace for which stats are to be displayed or null for the global value
     * @param cf ColumnFamily for which stats are to be displayed or null for the keyspace value (if ks supplied)
     * @param metricName View {@link TableMetrics}.
     */
    public Object getColumnFamilyMetric(String ks, String cf, String metricName)
    {
        try
        {
            ObjectName oName = null;
            if (!Strings.isNullOrEmpty(ks) && !Strings.isNullOrEmpty(cf))
            {
                String type = cf.contains(".") ? "IndexTable" : "Table";
                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=%s,keyspace=%s,scope=%s,name=%s", type, ks, cf, metricName));
            }
            else if (!Strings.isNullOrEmpty(ks))
            {
                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=Keyspace,keyspace=%s,name=%s", ks, metricName));
            }
            else
            {
                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=Table,name=%s", metricName));
            }
            switch(metricName)
            {
                case "BloomFilterDiskSpaceUsed":
                case "BloomFilterFalsePositives":
                case "BloomFilterFalseRatio":
                case "BloomFilterOffHeapMemoryUsed":
                case "IndexSummaryOffHeapMemoryUsed":
                case "CompressionDictionariesMemoryUsed":
                case "CompressionMetadataOffHeapMemoryUsed":
                case "CompressionRatio":
                case "EstimatedColumnCountHistogram":
                case "EstimatedPartitionSizeHistogram":
                case "EstimatedPartitionCount":
                case "KeyCacheHitRate":
                case "LiveSSTableCount":
                case "MaxSSTableDuration":
                case "MaxSSTableSize":
                case "OldVersionSSTableCount":
                case "MaxPartitionSize":
                case "MeanPartitionSize":
                case "MemtableColumnsCount":
                case "MemtableLiveDataSize":
                case "MemtableOffHeapSize":
                case "MinPartitionSize":
                case "PercentRepaired":
                case "BytesRepaired":
                case "BytesUnrepaired":
                case "BytesPendingRepair":
                case "RecentBloomFilterFalsePositives":
                case "RecentBloomFilterFalseRatio":
                case "SnapshotsSize":
                    return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                case "LiveDiskSpaceUsed":
                case "MemtableSwitchCount":
                case "SpeculativeRetries":
                case "TotalDiskSpaceUsed":
                case "WriteTotalLatency":
                case "ReadTotalLatency":
                case "PendingFlushes":
                {
                    // these are gauges for keyspace metrics, not counters
                    if (!Strings.isNullOrEmpty(ks) &&
                        Strings.isNullOrEmpty(cf) &&
                        (metricName.equals("TotalDiskSpaceUsed") ||
                         metricName.equals("LiveDiskSpaceUsed") ||
                         metricName.equals("MemtableSwitchCount")))
                    {
                        return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                    }
                    else
                    {
                        return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
                    }
                }
                case "CoordinatorReadLatency":
                case "CoordinatorScanLatency":
                case "ReadLatency":
                case "WriteLatency":
                    return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxTimerMBean.class);
                case "LiveScannedHistogram":
                case "SSTablesPerReadHistogram":
                case "TombstoneScannedHistogram":
                    return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxHistogramMBean.class);
                default:
                    throw new RuntimeException("Unknown table metric " + metricName);
            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieve Proxy metrics
     * @param scope RangeSlice, Read or Write
     */
    public CassandraMetricsRegistry.JmxTimerMBean getProxyMetric(String scope)
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn,
                    new ObjectName("org.apache.cassandra.metrics:type=ClientRequest,scope=" + scope + ",name=Latency"),
                    CassandraMetricsRegistry.JmxTimerMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public CassandraMetricsRegistry.JmxTimerMBean getMessagingQueueWaitMetrics(String verb)
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn,
                                     new ObjectName("org.apache.cassandra.metrics:name=" + verb + "-WaitLatency,type=Messaging"),
                                     CassandraMetricsRegistry.JmxTimerMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieve Proxy metrics
     * @param metricName BytesCompacted, CompactionsAborted, CompactionsReduced,
     *                   SSTablesDroppedFromCompaction, CompletedTasks, PendingTasks, PendingTasksByTableName
     *                   or TotalCompactionsCompleted.
     */
    public Object getCompactionMetric(String metricName)
    {
        try
        {
            switch(metricName)
            {
                case "BytesCompacted":
                case "CompressedBytesCompacted":
                case "CompactionsAborted":
                case "CompactionsReduced":
                case "SSTablesDroppedFromCompaction":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Compaction,name=" + metricName),
                            CassandraMetricsRegistry.JmxCounterMBean.class);
                case "CompletedTasks":
                case "PendingTasks":
                case "PendingTasksByTableName":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Compaction,name=" + metricName),
                            CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                case "TotalCompactionsCompleted":
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Compaction,name=" + metricName),
                            CassandraMetricsRegistry.JmxMeterMBean.class);
                default:
                    throw new RuntimeException("Unknown compaction metric " + metricName);
            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieve Proxy metrics
     * @param metricName
     */
    public Object getClientMetric(String metricName)
    {
        try
        {
            switch(metricName)
            {
                case "connections": // List<Map<String,String>> - list of all native connections and their properties
                case "connectedNativeClients": // number of connected native clients
                case "connectedNativeClientsByUser": // number of native clients by username
                case "clientsByProtocolVersion": // number of native clients by protocol version
                    return JMX.newMBeanProxy(mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=Client,name=" + metricName),
                            CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                default:
                    throw new RuntimeException("Unknown client metric " + metricName);
            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Object getCidrFilteringMetric(String metricName)
    {
        try
        {
            switch(metricName)
            {
                case CIDRAuthorizerMetrics.CIDR_CHECKS_LATENCY:
                    return JMX.newMBeanProxy(mbeanServerConn,
                                             new ObjectName("org.apache.cassandra.metrics:type=CIDRAuthorization,name="
                                                            + metricName),
                                             CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
                case CIDRAuthorizerMetrics.CIDR_GROUPS_CACHE_RELOAD_COUNT:
                    return JMX.newMBeanProxy(
                        mbeanServerConn,
                        new ObjectName("org.apache.cassandra.metrics:type=CIDRGroupsMappingCache,name=" + metricName),
                        CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
                case CIDRAuthorizerMetrics.CIDR_GROUPS_CACHE_RELOAD_LATENCY:
                case CIDRAuthorizerMetrics.LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY:
                    return JMX.newMBeanProxy(
                        mbeanServerConn,
                        new ObjectName("org.apache.cassandra.metrics:type=CIDRGroupsMappingCache,name=" + metricName),
                        CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
                default:
                    if (metricName.contains(CIDRAuthorizerMetrics.CIDR_ACCESSES_REJECTED_COUNT_PREFIX) ||
                        metricName.contains(CIDRAuthorizerMetrics.CIDR_ACCESSES_ACCEPTED_COUNT_PREFIX))
                    {
                        return JMX.newMBeanProxy(
                            mbeanServerConn,
                            new ObjectName("org.apache.cassandra.metrics:type=mymetricname,name=" + metricName),
                            CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
                    }

                    throw new RuntimeException("Unknown metric " + metricName);
            }
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Map<String, Long> getCountsMetricsFromVtable()
    {
        return cfmProxy.getCountsMetricsFromVtable();
    }

    public Map<String, List<Double>> getLatenciesMetricsFromVtable()
    {
        return cfmProxy.getLatenciesMetricsFromVtable();
    }

    /**
     * Retrieve Proxy metrics
     * @param metricName Exceptions, Load, TotalHints or TotalHintsInProgress.
     */
    public long getStorageMetric(String metricName)
    {
        try
        {
            return JMX.newMBeanProxy(mbeanServerConn,
                    new ObjectName("org.apache.cassandra.metrics:type=Storage,name=" + metricName),
                    CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
        }
        catch (MalformedObjectNameException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Double[] metricPercentilesAsArray(CassandraMetricsRegistry.JmxHistogramMBean metric)
    {
        return new Double[]{ metric.get50thPercentile(),
                Double.valueOf(metric.get75thPercentile()),
                Double.valueOf(metric.get95thPercentile()),
                Double.valueOf(metric.get98thPercentile()),
                Double.valueOf(metric.get99thPercentile()),
                Double.valueOf(metric.getMin()),
                Double.valueOf(metric.getMax())};
    }

    public Double[] metricPercentilesAsArray(CassandraMetricsRegistry.JmxTimerMBean metric)
    {
        return new Double[]{ Double.valueOf(metric.get50thPercentile()),
                             Double.valueOf(metric.get75thPercentile()),
                             Double.valueOf(metric.get95thPercentile()),
                             Double.valueOf(metric.get98thPercentile()),
                             Double.valueOf(metric.get99thPercentile()),
                             Double.valueOf(metric.getMin()),
                             Double.valueOf(metric.getMax())};
    }

    public TabularData getCompactionHistory()
    {
        return compactionProxy.getCompactionHistory();
    }

    public void reloadTriggers()
    {
        spProxy.reloadTriggerClasses();
    }

    public void setLoggingLevel(String classQualifier, String level)
    {
        try
        {
            ssProxy.setLoggingLevel(classQualifier, level);
        }
        catch (Exception e)
        {
            throw new RuntimeException("Error setting log for " + classQualifier + " on level " + level + ". Please check logback configuration.", e);
        }
    }

    public Map<String, String> getLoggingLevels()
    {
        return ssProxy.getLoggingLevels();
    }

    public long getPid()
    {
        return NativeLibrary.getProcessID();
    }

    public void resumeBootstrap(PrintStream out) throws IOException
    {
        BootstrapMonitor monitor = new BootstrapMonitor(out);
        try
        {
            if (jmxc != null)
                jmxc.addConnectionNotificationListener(monitor, null, null);
            ssProxy.addNotificationListener(monitor, null, null);
            if (ssProxy.resumeBootstrap())
            {
                out.println("Resuming bootstrap");
                monitor.awaitCompletion();
                if (monitor.getError() != null)
                    throw monitor.getError();
            }
            else
            {
                out.println("Node is already bootstrapped.");
            }
        }
        catch (Exception e)
        {
            throw new IOException(e);
        }
        finally
        {
            try
            {
                ssProxy.removeNotificationListener(monitor);
                if (jmxc != null)
                    jmxc.removeConnectionNotificationListener(monitor);
            }
            catch (Throwable e)
            {
                out.println("Exception occurred during clean-up. " + e);
            }
        }
    }

    public Map<String, List<Integer>> getMaximumPoolSizes(List<String> stageNames)
    {
        return ssProxy.getConcurrency(stageNames);
    }

    public void setConcurrency(String stageName, int coreThreads, int maxConcurrency)
    {
        ssProxy.setConcurrency(stageName, coreThreads, maxConcurrency);
    }

    public void replayBatchlog() throws IOException
    {
        try
        {
            bmProxy.forceBatchlogReplay();
        }
        catch (Exception e)
        {
            throw new IOException(e);
        }
    }

    public TabularData getFailureDetectorPhilValues(boolean withPort)
    {
        try
        {
            return withPort ? fdProxy.getPhiValuesWithPort() : fdProxy.getPhiValues();
        }
        catch (OpenDataException e)
        {
            throw new RuntimeException(e);
        }
    }

    public ActiveRepairServiceMBean getRepairServiceProxy()
    {
        return arsProxy;
    }

    public void reloadSslCerts() throws IOException
    {
        msProxy.reloadSslCertificates();
    }

    public void clearConnectionHistory()
    {
        ssProxy.clearConnectionHistory();
    }

    public void disableAuditLog()
    {
        ssProxy.disableAuditLog();
    }

    public void enableAuditLog(String loggerName, Map<String, String> parameters, String includedKeyspaces, String excludedKeyspaces,
                               String includedCategories, String excludedCategories, String includedUsers, String excludedUsers)
    {
        ssProxy.enableAuditLog(loggerName, parameters, includedKeyspaces, excludedKeyspaces, includedCategories, excludedCategories, includedUsers, excludedUsers);
    }

    public void enableAuditLog(String loggerName, String includedKeyspaces, String excludedKeyspaces, String includedCategories,
                               String excludedCategories, String includedUsers, String excludedUsers)
    {
        this.enableAuditLog(loggerName, Collections.emptyMap(), includedKeyspaces, excludedKeyspaces, includedCategories, excludedCategories, includedUsers, excludedUsers);
    }

    public void enableAuditLog(String loggerName, Map<String, String> parameters, String includedKeyspaces, String excludedKeyspaces, String includedCategories, String excludedCategories,
                               String includedUsers, String excludedUsers, Integer maxArchiveRetries, Boolean block, String rollCycle,
                               Long maxLogSize, Integer maxQueueWeight, String archiveCommand)
    {
        ssProxy.enableAuditLog(loggerName, parameters, includedKeyspaces, excludedKeyspaces, includedCategories, excludedCategories, includedUsers, excludedUsers,
                               maxArchiveRetries, block, rollCycle, maxLogSize, maxQueueWeight, archiveCommand);
    }

    public void enableOldProtocolVersions()
    {
        ssProxy.enableNativeTransportOldProtocolVersions();
    }

    public void disableOldProtocolVersions()
    {
        ssProxy.disableNativeTransportOldProtocolVersions();
	}

    public MessagingServiceMBean getMessagingServiceProxy()
    {
        return msProxy;
    }

    public void enableFullQueryLogger(String path, String rollCycle, Boolean blocking, int maxQueueWeight, long maxLogSize, @Nullable String archiveCommand, int maxArchiveRetries)
    {
        ssProxy.enableFullQueryLogger(path, rollCycle, blocking, maxQueueWeight, maxLogSize, archiveCommand, maxArchiveRetries);
    }

    public void stopFullQueryLogger()
    {
        ssProxy.stopFullQueryLogger();
    }

    public void resetFullQueryLogger()
    {
        ssProxy.resetFullQueryLogger();
    }

    public FullQueryLoggerOptions getFullQueryLoggerOptions()
    {
        return FullQueryLoggerOptionsCompositeData.fromCompositeData(ssProxy.getFullQueryLoggerOptions());
    }

    public AuditLogOptions getAuditLogOptions()
    {
        return AuditLogOptionsCompositeData.fromCompositeData(almProxy.getAuditLogOptionsData());
    }

    public void setDefaultKeyspaceReplicationFactor(int value)
    {
        ssProxy.setDefaultKeyspaceReplicationFactor(value);
    }

    public int getDefaultKeyspaceReplicationFactor()
    {
        return ssProxy.getDefaultKeyspaceReplicationFactor();
    }

    public void abortBootstrap(String nodeId, String endpoint)
    {
        ssProxy.abortBootstrap(nodeId, endpoint);
    }

    public boolean isAutoRepairDisabled()
    {
        return autoRepairProxy.isAutoRepairDisabled();
    }

    public String autoRepairConfiguration()
    {
        return autoRepairProxy.getAutoRepairConfiguration();
    }

    public void setAutoRepairTokenRangeSplitterParameter(String repairType, String key, String value)
    {
        autoRepairProxy.setAutoRepairTokenRangeSplitterParameter(repairType, key, value);
    }

    public void setAutoRepairEnabled(String repairType, boolean enabled)
    {
        autoRepairProxy.setAutoRepairEnabled(repairType, enabled);
    }

    public void setAutoRepairThreads(String repairType, int repairThreads)
    {
        autoRepairProxy.setRepairThreads(repairType, repairThreads);
    }

    public void setAutoRepairPriorityForHosts(String repairType, String commaSeparatedHostSet)
    {
        autoRepairProxy.setRepairPriorityForHosts(repairType, commaSeparatedHostSet);
    }

    public void setAutoRepairForceRepairForHosts(String repairType, String commaSeparatedHostSet)
    {
        autoRepairProxy.setForceRepairForHosts(repairType, commaSeparatedHostSet);
    }

    public void setAutoRepairMinInterval(String repairType, String minRepairInterval)
    {
        autoRepairProxy.setRepairMinInterval(repairType, minRepairInterval);
    }

    public void setAutoRepairHistoryClearDeleteHostsBufferDuration(String duration)
    {
        autoRepairProxy.setAutoRepairHistoryClearDeleteHostsBufferDuration(duration);
    }

    public void startAutoRepairScheduler()
    {
        autoRepairProxy.startScheduler();
    }

    public void setAutoRepairMinRepairTaskDuration(String duration)
    {
        autoRepairProxy.setAutoRepairMinRepairTaskDuration(duration);
    }

    public void setAutoRepairSSTableCountHigherThreshold(String repairType, int ssTableHigherThreshold)
    {
        autoRepairProxy.setRepairSSTableCountHigherThreshold(repairType, ssTableHigherThreshold);
    }

    public void setAutoRepairTableMaxRepairTime(String repairType, String autoRepairTableMaxRepairTime)
    {
        autoRepairProxy.setAutoRepairTableMaxRepairTime(repairType, autoRepairTableMaxRepairTime);
    }

    public void setAutoRepairIgnoreDCs(String repairType, Set<String> ignoreDCs)
    {
        autoRepairProxy.setIgnoreDCs(repairType, ignoreDCs);
    }

    public void setAutoRepairParallelRepairPercentage(String repairType, int percentage)
    {
        autoRepairProxy.setParallelRepairPercentage(repairType, percentage);
    }

    public void setAutoRepairParallelRepairCount(String repairType, int count)
    {
        autoRepairProxy.setParallelRepairCount(repairType, count);
    }

    public void setAutoRepairAllowParallelReplicaRepair(String repairType, boolean enabled)
    {
        autoRepairProxy.setAllowParallelReplicaRepair(repairType, enabled);
    }

    public void setAutoRepairAllowParallelReplicaRepairAcrossSchedules(String repairType, boolean enabled)
    {
        autoRepairProxy.setAllowParallelReplicaRepairAcrossSchedules(repairType, enabled);
    }

    public void setAutoRepairPrimaryTokenRangeOnly(String repairType, boolean primaryTokenRangeOnly)
    {
        autoRepairProxy.setPrimaryTokenRangeOnly(repairType, primaryTokenRangeOnly);
    }

    public void setAutoRepairMaterializedViewRepairEnabled(String repairType, boolean enabled)
    {
        autoRepairProxy.setMVRepairEnabled(repairType, enabled);
    }

    public List<String> mutateSSTableRepairedState(boolean repair, boolean preview, String keyspace, List<String> tables)
    {
        return ssProxy.mutateSSTableRepairedState(repair, preview, keyspace, tables);
    }

    public List<String> getAutoRepairTablesForKeyspace(String keyspace)
    {
        return ssProxy.getTablesForKeyspace(keyspace);
    }

    public void setAutoRepairSessionTimeout(String repairType, String timeout)
    {
        autoRepairProxy.setRepairSessionTimeout(repairType, timeout);
    }

    public Set<String> getAutoRepairOnGoingRepairHostIds(String repairType)
    {
        return autoRepairProxy.getOnGoingRepairHostIds(repairType);
    }

    public void setAutoRepairRepairByKeyspace(String repairType, boolean enabled)
    {
        autoRepairProxy.setRepairByKeyspace(repairType, enabled);
    }

    public void setAutoRepairMaxRetriesCount(String repairType, int retries)
    {
        autoRepairProxy.setAutoRepairMaxRetriesCount(repairType, retries);
    }

    public void setAutoRepairRetryBackoff(String repairType, String interval)
    {
        autoRepairProxy.setAutoRepairRetryBackoff(repairType, interval);
    }

    public GuardrailsMBean getGuardrailsMBean()
    {
        return grProxy;
    }

    public void setMixedMajorVersionRepairEnabled(boolean enabled)
    {
        autoRepairProxy.setMixedMajorVersionRepairEnabled(enabled);
    }

    /**
     * Triggers compression dictionary training for the specified table.
     * Samples chunks from existing SSTables and trains a dictionary.
     *
     * @param keyspace                   the keyspace name
     * @param table                      the table name
     * @param force                      force the dictionary training even if there are not enough samples
     * @param parameters                 training parameters, if empty, training parameters will be taken from CQL's
     *                                   compression section of a given table training is conducted on.
     * @throws IOException               if there's an error accessing the MBean
     * @throws IllegalArgumentException  if table doesn't support dictionary compression
     */
    public void trainCompressionDictionary(String keyspace, String table,
                                           boolean force,
                                           Map<String, String> parameters) throws IOException
    {
        doWithCompressionDictionaryManagerMBean(proxy -> {proxy.train(force, parameters); return null; }, keyspace, table);
    }

    /**
     * Returns latest dictionary for given keyspace and table.
     *
     * @param keyspace the keyspace name
     * @param table the table name
     * @return the latest dictionary for given keyspace and table
     * @throws IOException if there's an error accessing the MBean
     * @throws IllegalArgumentException if table doesn't support dictionary compression
     */
    public CompositeData getCompressionDictionary(String keyspace, String table) throws IOException
    {
        return doWithCompressionDictionaryManagerMBean(CompressionDictionaryManagerMBean::getCompressionDictionary, keyspace, table);
    }

    /**
     * Returns the dictionary for given keyspace and table and dictionary id.
     *
     * @param keyspace the keyspace name
     * @param table the table name
     * @param dictId id of dictionary to get
     * @return the dictionary for given keyspace and table and dictionary id.
     * @throws IOException if there's an error accessing the MBean
     * @throws IllegalArgumentException if table doesn't support dictionary compression
     */
    public CompositeData getCompressionDictionary(String keyspace, String table, long dictId) throws IOException
    {
        return doWithCompressionDictionaryManagerMBean(proxy -> proxy.getCompressionDictionary(dictId), keyspace, table);
    }

    /**
     * Imports dictionary in composite data to database.
     *
     * @param compositeData data to import
     * @throws IOException if there's an error accessing the MBean
     * @throws IllegalArgumentException if keyspace and table values in compositeData are missing
     * or if table doesn't support dictionary compression
     */
    public void importCompressionDictionary(CompositeData compositeData) throws IOException
    {
        String keyspace = (String) compositeData.get(CompressionDictionaryDetailsTabularData.KEYSPACE_NAME);
        String table = (String) compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_NAME);

        if (keyspace == null || table == null)
        {
            throw new IllegalArgumentException("Argument must have keyspace and table values.");
        }

        doWithCompressionDictionaryManagerMBean(proxy -> { proxy.importCompressionDictionary(compositeData); return null; }, keyspace, table);
    }

    /**
     *
     * @param keyspace keyspace to list dictionaries for
     * @param table table to list dictionaries for
     * @return tabular data with listing
     * @throws IOException if there's an error accessing the MBean
     * @throws IllegalArgumentException if table doesn't support dictionary compression
     */
    public TabularData listCompressionDictionaries(String keyspace, String table) throws IOException
    {
        return doWithCompressionDictionaryManagerMBean(CompressionDictionaryManagerMBean::listCompressionDictionaries, keyspace, table);
    }

    private <T> T doWithCompressionDictionaryManagerMBean(Function<CompressionDictionaryManagerMBean, T> func,
                                                          String keyspace, String table) throws IOException
    {
        try
        {
            return func.apply(getDictionaryManagerProxy(keyspace, table));
        }
        catch (Exception e)
        {
            if (e.getCause() instanceof InstanceNotFoundException)
            {
                String message = String.format("Table %s.%s does not exist or does not support dictionary compression",
                                               keyspace, table);
                throw new IllegalArgumentException(message);
            }
            else
            {
                throw new IOException(e.getMessage());
            }
        }
    }

    /**
     * Gets the compression dictionary training state for the specified table.
     * Returns an atomic snapshot of training status, progress, and failure details.
     *
     * @param keyspace the keyspace name
     * @param table the table name
     * @return the current training state
     * @throws IOException if there's an error accessing the MBean
     */
    public TrainingState getCompressionDictionaryTrainingState(String keyspace, String table) throws IOException
    {
        CompositeData compositeData = getDictionaryManagerProxy(keyspace, table).getTrainingState();
        return TrainingState.fromCompositeData(compositeData);
    }

    /**
     * Gets all compression dictionaries which are orphaned, that is a table they were trained for was dropped.
     * @return tabular data of orphaned dictionaries
     */
    public TabularData getOrphanedCompressionDictionaries()
    {
        return ssProxy.getOrphanedCompressionDictionaries();
    }

    public void clearOrphanedCompressionDictionaries()
    {
        ssProxy.clearOrphanedCompressionDictionaries();
    }

    private CompressionDictionaryManagerMBean getDictionaryManagerProxy(String keyspace, String table) throws IOException
    {
        // Construct table-specific MBean name
        String mbeanName = CompressionDictionaryManagerMBean.MBEAN_NAME + ",keyspace=" + keyspace + ",table=" + table;
        try
        {
            ObjectName objectName = new ObjectName(mbeanName);
            return JMX.newMBeanProxy(mbeanServerConn, objectName, CompressionDictionaryManagerMBean.class);
        }
        catch (MalformedObjectNameException e)
        {
            throw new IOException("Invalid keyspace or table name", e);
        }
    }
}

class ColumnFamilyStoreMBeanIterator implements Iterator<Map.Entry<String, ColumnFamilyStoreMBean>>
{
    private MBeanServerConnection mbeanServerConn;
    Iterator<Entry<String, ColumnFamilyStoreMBean>> mbeans;

    public ColumnFamilyStoreMBeanIterator(MBeanServerConnection mbeanServerConn)
        throws MalformedObjectNameException, NullPointerException, IOException
    {
        this.mbeanServerConn = mbeanServerConn;
        List<Entry<String, ColumnFamilyStoreMBean>> cfMbeans = getCFSMBeans(mbeanServerConn, "ColumnFamilies");
        cfMbeans.addAll(getCFSMBeans(mbeanServerConn, "IndexColumnFamilies"));
        Collections.sort(cfMbeans, new Comparator<Entry<String, ColumnFamilyStoreMBean>>()
        {
            public int compare(Entry<String, ColumnFamilyStoreMBean> e1, Entry<String, ColumnFamilyStoreMBean> e2)
            {
                //compare keyspace, then CF name, then normal vs. index
                int keyspaceNameCmp = e1.getKey().compareTo(e2.getKey());
                if(keyspaceNameCmp != 0)
                    return keyspaceNameCmp;

                // get CF name and split it for index name
                String e1CF[] = e1.getValue().getTableName().split("\\.");
                String e2CF[] = e2.getValue().getTableName().split("\\.");
                assert e1CF.length <= 2 && e2CF.length <= 2 : "unexpected split count for table name";

                //if neither are indexes, just compare CF names
                if(e1CF.length == 1 && e2CF.length == 1)
                    return e1CF[0].compareTo(e2CF[0]);

                //check if it's the same CF
                int cfNameCmp = e1CF[0].compareTo(e2CF[0]);
                if(cfNameCmp != 0)
                    return cfNameCmp;

                // if both are indexes (for the same CF), compare them
                if(e1CF.length == 2 && e2CF.length == 2)
                    return e1CF[1].compareTo(e2CF[1]);

                //if length of e1CF is 1, it's not an index, so sort it higher
                return e1CF.length == 1 ? 1 : -1;
            }
        });
        mbeans = cfMbeans.iterator();
    }

    private List<Entry<String, ColumnFamilyStoreMBean>> getCFSMBeans(MBeanServerConnection mbeanServerConn, String type)
            throws MalformedObjectNameException, IOException
    {
        ObjectName query = new ObjectName("org.apache.cassandra.db:type=" + type +",*");
        Set<ObjectName> cfObjects = mbeanServerConn.queryNames(query, null);
        List<Entry<String, ColumnFamilyStoreMBean>> mbeans = new ArrayList<Entry<String, ColumnFamilyStoreMBean>>(cfObjects.size());
        for(ObjectName n : cfObjects)
        {
            String keyspaceName = n.getKeyProperty("keyspace");
            ColumnFamilyStoreMBean cfsProxy = JMX.newMBeanProxy(mbeanServerConn, n, ColumnFamilyStoreMBean.class);
            mbeans.add(new AbstractMap.SimpleImmutableEntry<String, ColumnFamilyStoreMBean>(keyspaceName, cfsProxy));
        }
        return mbeans;
    }

    public boolean hasNext()
    {
        return mbeans.hasNext();
    }

    public Entry<String, ColumnFamilyStoreMBean> next()
    {
        return mbeans.next();
    }

    public void remove()
    {
        throw new UnsupportedOperationException();
    }
}
