<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/map/ZrangeCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.map

import com.aerospike.client.Key
import com.aerospike.client.Operation
import com.aerospike.client.Record
import com.aerospike.client.Value
import com.aerospike.client.cdt.MapOperation
import com.aerospike.client.cdt.MapReturnType
import com.aerospike.client.listener.RecordListener
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import com.aerospike.skyhook.util.Intervals
import io.netty.channel.ChannelHandlerContext
import java.util.*

open class ZrangeCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx), RecordListener {

    protected data class LimitArgument(
        val offset: Int,
        val count: Int,
    )

    protected class RangeCommand(val cmd: RequestCommand, flagIndex: Int) {
        var BYSCORE: Boolean = false
        var BYLEX: Boolean = false
        var REV: Boolean = false
        var LIMIT: LimitArgument? = null
            private set
        var WITHSCORES: Boolean = false

        val minScore: Int by lazy {
            Intervals.fromScore(String(cmd.args[flagIndex - 2]))
        }

        val maxScore: Int by lazy {
            Intervals.upScore(String(cmd.args[flagIndex - 1]))
        }

        val minLex: String by lazy {
            Intervals.fromLex(String(cmd.args[flagIndex - 2]))
        }

        val maxLex: String by lazy {
            Intervals.upLex(String(cmd.args[flagIndex - 1]))
        }

        val factor: Long by lazy {
            if (WITHSCORES) 2L else 1L
        }

        init {
            var i = flagIndex
            while (i < cmd.args.size) {
                i += setFlag(i)
            }
        }

        private fun setFlag(i: Int): Int {
            val flagStr = String(cmd.args[i])
            when (flagStr.uppercase(Locale.ENGLISH)) {
                "BYSCORE" -> BYSCORE = true
                "BYLEX" -> BYLEX = true
                "REV" -> REV = true
                "WITHSCORES" -> WITHSCORES = true
                "LIMIT" -> {
                    LIMIT = LimitArgument(
                        String(cmd.args[i + 1]).toInt(),
                        String(cmd.args[i + 2]).toInt(),
                    )
                    return 3
                }
                else -> throw IllegalArgumentException(flagStr)
            }
            return 1
        }
    }

    @Volatile
    protected lateinit var rangeCommand: RangeCommand

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount >= 4) { argValidationErrorMsg(cmd) }

        val key = createKey(cmd.key)
        rangeCommand = RangeCommand(cmd, 4)
        validateAndSet()

        client.operate(
            null, this, defaultWritePolicy,
            key, getMapOperation()
        )
    }

    protected open fun validateAndSet() {
        require(!(rangeCommand.BYSCORE && rangeCommand.BYLEX)) { "[BYSCORE|BYLEX]" }
    }

    protected open fun getMapReturnType(): Int {
        return if (rangeCommand.WITHSCORES) {
            MapReturnType.KEY_VALUE
        } else {
            MapReturnType.KEY
        }
    }

    protected open fun getMapOperation(): Operation {
        return when {
            rangeCommand.BYSCORE -> {
                MapOperation.getByValueRange(
                    aeroCtx.bin,
                    Value.get(rangeCommand.minScore),
                    Value.get(rangeCommand.maxScore), getMapReturnType()
                )
            }
            rangeCommand.BYLEX -> {
                MapOperation.getByKeyRange(
                    aeroCtx.bin,
                    Value.get(rangeCommand.minLex),
                    Value.get(rangeCommand.maxLex), getMapReturnType()
                )
            }
            else -> {
                val from = if (rangeCommand.minScore == Int.MIN_VALUE) 0 else rangeCommand.minScore
                val count = rangeCommand.maxScore - from
                MapOperation.getByIndexRange(
                    aeroCtx.bin,
                    from,
                    count, getMapReturnType()
                )
            }
        }
    }

    override fun onSuccess(key: Key?, record: Record?) {
        try {
            if (record == null) {
                writeEmptyList()
            } else {
                writeResponse(record.bins[aeroCtx.bin])
            }
            flushCtxTransactionAware()
        } catch (e: Exception) {
            closeCtx(e)
        }
    }

    override fun writeResponse(mapped: Any?) {
        when (mapped) {
            is Map<*, *> -> applyFilters(mapped.toList()).forEach {
                super.writeResponse(it.first.toString())
                super.writeResponse(it.second.toString())
            }
            is List<*> -> when (mapped.firstOrNull()) {
                is Map.Entry<*, *> -> applyFilters(mapped)
                    .map { it as Map.Entry<*, *> }.map { it.toPair() }
                    .forEach {
                        super.writeResponse(it.first.toString())
                        super.writeResponse(it.second.toString())
                    }
                else -> applyFilters(mapped).forEach {
                    super.writeResponse(it.toString())
                }
            }
        }
    }

    private fun <T> applyFilters(list: List<T>): List<T> {
        return list.let {
            if (rangeCommand.REV) {
                it.reversed()
            } else {
                it
            }
        }.let { l ->
            rangeCommand.LIMIT?.let {
                l.subList(it.offset, it.offset + it.count)
            } ?: l
        }.also {
            writeArrayHeader(it.size * rangeCommand.factor)
        }
    }

    protected fun validateCommon() {
        require(!rangeCommand.BYSCORE) { "BYSCORE flag not supported" }
        require(!rangeCommand.BYLEX) { "BYLEX flag not supported" }
        require(!rangeCommand.REV) { "REV flag not supported" }
    }
}

class ZrevrangeCommandListener(
    ctx: ChannelHandlerContext
) : ZrangeCommandListener(ctx) {

    override fun validateAndSet() {
        validateCommon()
        require(rangeCommand.LIMIT?.let { false } ?: true) { "LIMIT flag not supported" }

        rangeCommand.REV = true
    }
}

open class ZrangebyscoreCommandListener(
    ctx: ChannelHandlerContext
) : ZrangeCommandListener(ctx) {

    override fun validateAndSet() {
        validateCommon()

        rangeCommand.BYSCORE = true
    }
}

class ZrevrangebyscoreCommandListener(
    ctx: ChannelHandlerContext
) : ZrangebyscoreCommandListener(ctx) {

    override fun validateAndSet() {
        super.validateAndSet()

        rangeCommand.REV = true
    }
}

open class ZrangebylexCommandListener(
    ctx: ChannelHandlerContext
) : ZrangeCommandListener(ctx) {

    override fun validateAndSet() {
        validateCommon()
        require(!rangeCommand.WITHSCORES) { "WITHSCORES flag not supported" }

        rangeCommand.BYLEX = true
    }
}

class ZrevrangebylexCommandListener(
    ctx: ChannelHandlerContext
) : ZrangebylexCommandListener(ctx) {

    override fun validateAndSet() {
        super.validateAndSet()

        rangeCommand.REV = true
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/util/Typed.kt<|end_filename|>
package com.aerospike.skyhook.util

import com.aerospike.client.Value
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets

object Typed {

    fun getValue(wireVal: ByteArray): Value {
        try {
            return Value.DoubleValue(String(wireVal).toDouble())
        } catch (e: NumberFormatException) {
        }
        try {
            StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(wireVal))
        } catch (ex: CharacterCodingException) {
            return Value.BytesValue(wireVal)
        }
        return Value.StringValue(wireVal.toString(Charsets.UTF_8))
    }

    fun getStringValue(wireVal: ByteArray): Value {
        return Value.StringValue(wireVal.toString(Charsets.UTF_8))
    }

    fun getInteger(wireVal: ByteArray): Int {
        return String(wireVal).toInt()
    }

    fun getLong(wireVal: ByteArray): Long {
        return String(wireVal).toLong()
    }

    fun getDouble(wireVal: ByteArray): Double {
        return String(wireVal).toDouble()
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/key/GetCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.key

import com.aerospike.client.Key
import com.aerospike.client.Operation
import com.aerospike.client.Record
import com.aerospike.client.listener.RecordListener
import com.aerospike.client.policy.WritePolicy
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import com.aerospike.skyhook.listener.ValueType
import io.netty.channel.ChannelHandlerContext
import java.util.*

open class GetCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx), RecordListener {

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount == 2) { argValidationErrorMsg(cmd) }

        val key = createKey(cmd.key)
        client.get(null, this, defaultWritePolicy, key)
    }

    override fun onSuccess(key: Key?, record: Record?) {
        if (record?.bins == null) {
            writeNullString()
            flushCtxTransactionAware()
        } else {
            try {
                val value = record.bins[aeroCtx.bin]
                writeResponse(
                    when (value) {
                        is Long -> value.toString()
                        else -> value
                    }
                )
                flushCtxTransactionAware()
            } catch (e: Exception) {
                closeCtx(e)
            }
        }
    }
}

class GetexCommandListener(
    ctx: ChannelHandlerContext
) : GetCommandListener(ctx) {

    private class GetexCommand(val cmd: RequestCommand) {
        var EX: Int? = null
        var PX: Int? = null
        var EXAT: Long? = null
            private set
        var PXAT: Long? = null
            private set
        var PERSIST: Boolean? = null
            private set

        init {
            for (i in 2 until cmd.args.size) {
                setFlag(i)
            }
            validate()
        }

        fun buildPolicy(policy: WritePolicy): WritePolicy {
            EX?.let {
                require(it > 0) { "invalid expiration" }
                policy.expiration = it
            }
            PX?.let {
                require(it > 0) { "invalid expiration" }
                policy.expiration = Integer.max(it / 1000, 1)
            }
            EXAT?.let {
                val exp = it - (System.currentTimeMillis() / 1000)
                require(exp > 0) { "invalid expiration" }
                policy.expiration = exp.toInt()
            }
            PXAT?.let {
                val exp = it - System.currentTimeMillis()
                require(exp > 0) { "invalid expiration" }
                policy.expiration = Integer.max(exp.toInt() / 1000, 1)
            }
            PERSIST?.let {
                policy.expiration = 0
            }
            return policy
        }

        private fun setFlag(i: Int) {
            val flagStr = String(cmd.args[i])
            when (flagStr.uppercase(Locale.ENGLISH)) {
                "EX" -> EX = String(cmd.args[i + 1]).toInt()
                "PX" -> PX = String(cmd.args[i + 1]).toInt()
                "EXAT" -> EXAT = String(cmd.args[i + 1]).toLong()
                "PXAT" -> PXAT = String(cmd.args[i + 1]).toLong()
                "PERSIST" -> PERSIST = true
            }
        }

        private fun validate() {
            require(listOfNotNull(EX, PX, EXAT, PXAT, PERSIST).size <= 1) {
                "[EX|PX|EXAT|PXAT|PERSIST]"
            }
        }
    }

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) }

        val getexCommand = GetexCommand(cmd)
        val key = createKey(cmd.key)
        val ops = arrayOf(
            *systemOps(ValueType.STRING),
            Operation.get(aeroCtx.bin)
        )
        val writePolicy = getexCommand.buildPolicy(getWritePolicy())

        client.operate(null, this, writePolicy, key, *ops)
    }
}


<|start_filename|>src/test/kotlin/com/aerospike/skyhook/SetCommandsTest.kt<|end_filename|>
package com.aerospike.skyhook

import com.aerospike.skyhook.command.RedisCommand
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SetCommandsTest() : SkyhookIntegrationTestBase() {

    private val _key = "set"

    private fun setup(n: Int = 3, key: String = _key) {
        for (i in 1..n) {
            writeCommand("${RedisCommand.SADD.name} $key val$i")
            assertEquals(1, readLong())
        }
    }

    @Test
    fun testSadd() {
        setup(2)
        writeCommand("${RedisCommand.SADD.name} $_key val1")
        assertEquals(0, readLong())
    }

    @Test
    fun testSismember() {
        setup(1)
        writeCommand("${RedisCommand.SISMEMBER.name} $_key val1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.SISMEMBER.name} $_key val2")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.SISMEMBER.name} ne val1")
        assertEquals(0, readLong())
    }

    @Test
    fun testSmismember() {
        setup(1)
        writeCommand("${RedisCommand.SMISMEMBER.name} $_key val1 val2 val3")
        val r = readLongArray()
        assertEquals(1, r[0])
        assertEquals(0, r[1])
        assertEquals(0, r[2])

        writeCommand("${RedisCommand.SMISMEMBER.name} ne val1 val2")
        val r2 = readLongArray()
        assertEquals(0, r2[0])
        assertEquals(0, r2[1])
    }

    @Test
    fun testSmembers() {
        setup()
        writeCommand("${RedisCommand.SMEMBERS.name} $_key")
        val r = readStringArray()
        assertEquals("val1", r[0])
        assertEquals("val2", r[1])
        assertEquals("val3", r[2])
    }

    @Test
    fun testScard() {
        setup()
        writeCommand("${RedisCommand.SCARD.name} $_key")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.SCARD.name} set2")
        assertEquals(0, readLong())
    }

    @Test
    fun testSrem() {
        setup(1)
        writeCommand("${RedisCommand.SREM.name} $_key val1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.SREM.name} $_key val2")
        assertEquals(0, readLong())
    }

    @Test
    fun testSunion() {
        setup()
        setup(4, "set2")
        writeCommand("${RedisCommand.SUNION.name} $_key set2")
        val r = readStringArray()
        assertTrue { r.size == 4 }
        assertTrue { r.contains("val1") }
        assertTrue { r.contains("val2") }
        assertTrue { r.contains("val3") }
        assertTrue { r.contains("val4") }
    }

    @Test
    fun testSunionstore() {
        setup()
        setup(4, "set2")
        writeCommand("${RedisCommand.SUNIONSTORE.name} union $_key set2")
        assertEquals(4, readLong())
    }

    @Test
    fun testSinter() {
        setup()
        setup(4, "set2")
        writeCommand("${RedisCommand.SINTER.name} $_key set2")
        val r = readStringArray()
        assertTrue { r.size == 3 }
        assertTrue { r.contains("val1") }
        assertTrue { r.contains("val2") }
        assertTrue { r.contains("val3") }
    }

    @Test
    fun testSinterstore() {
        setup()
        setup(4, "set2")
        writeCommand("${RedisCommand.SINTERSTORE.name} inter $_key set2")
        assertEquals(3, readLong())
    }

    @Test
    fun testSrandmember() {
        setup()
        writeCommand("${RedisCommand.SRANDMEMBER.name} $_key")
        val r = readFullBulkString()
        assertTrue { r.startsWith("val") }

        writeCommand("${RedisCommand.SRANDMEMBER.name} $_key 5")
        val r2 = readStringArray()
        assertTrue { r2.size == 3 }

        writeCommand("${RedisCommand.SRANDMEMBER.name} $_key -5")
        val r3 = readStringArray()
        assertTrue { r3.size == 5 }

        writeCommand("${RedisCommand.SRANDMEMBER.name} ne")
        assertEquals(nullString, readFullBulkString())
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/util/RegexUtils.kt<|end_filename|>
package com.aerospike.skyhook.util

object RegexUtils {

    fun format(regex: String): String {
        return regex.replace("*", ".*")
            .replace("?", ".")
    }
}


<|start_filename|>src/test/kotlin/com/aerospike/skyhook/SortedSetCommandsTest.kt<|end_filename|>
package com.aerospike.skyhook

import com.aerospike.skyhook.command.RedisCommand
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SortedSetCommandsTest() : SkyhookIntegrationTestBase() {

    private val _key = "zset"

    private fun setup(n: Int = 3, key: String = _key, v: String = "val") {
        for (i in 1..n) {
            writeCommand("${RedisCommand.ZADD.name} $key $i $v$i")
            assertEquals(1, readLong())
        }
    }

    @Test
    fun testZmscore() {
        setup()
        writeCommand("${RedisCommand.ZMSCORE.name} $_key val1 val2 val3")
        val r = readStringArray()
        assertTrue { r.size == 3 }
        assertEquals("1", r[0])
        assertEquals("2", r[1])
        assertEquals("3", r[2])
    }

    @Test
    fun testZcard() {
        setup()
        writeCommand("${RedisCommand.ZCARD.name} $_key")
        assertEquals(3, readLong())
    }

    @Test
    fun testZrem() {
        setup()
        writeCommand("${RedisCommand.ZREM.name} $_key val1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.ZREM.name} $_key val11")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.ZCARD.name} $_key")
        assertEquals(2, readLong())
    }

    @Test
    fun testZincby() {
        setup(1)
        writeCommand("${RedisCommand.ZINCRBY.name} $_key 10 val1")
        assertEquals("11", readFullBulkString())
    }

    @Test
    fun testZrank() {
        setup()
        setup(3, _key, "v")
        writeCommand("${RedisCommand.ZRANK.name} $_key v1")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.ZRANK.name} $_key val3")
        assertEquals(5, readLong())
        writeCommand("${RedisCommand.ZRANK.name} $_key val11")
        assertEquals(nullString, readFullBulkString())
        writeCommand("${RedisCommand.ZRANK.name} ne val1")
        assertEquals(nullString, readFullBulkString())
    }

    @Test
    fun testZpopmax() {
        setup()
        setup(3, _key, "v")
        writeCommand("${RedisCommand.ZPOPMAX.name} $_key 3")
        val r = readStringArray()
        assertTrue { r.size == 6 }
        assertEquals("val3", r[0])
        assertEquals("3", r[1])
        assertEquals("v3", r[2])
        assertEquals("3", r[3])
        assertEquals("val2", r[4])
        assertEquals("2", r[5])

        writeCommand("${RedisCommand.ZPOPMAX.name} $_key")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("v2", r2[0])
        assertEquals("2", r2[1])

        writeCommand("${RedisCommand.ZPOPMAX.name} $_key 0")
        val r3 = readStringArray()
        assertTrue { r3.isEmpty() }
    }

    @Test
    fun testZpopmain() {
        setup()
        setup(3, _key, "v")
        writeCommand("${RedisCommand.ZPOPMIN.name} $_key 3")
        val r = readStringArray()
        assertTrue { r.size == 6 }
        assertEquals("v1", r[0])
        assertEquals("1", r[1])
        assertEquals("val1", r[2])
        assertEquals("1", r[3])
        assertEquals("v2", r[4])
        assertEquals("2", r[5])

        writeCommand("${RedisCommand.ZPOPMIN.name} $_key")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val2", r2[0])
        assertEquals("2", r2[1])

        writeCommand("${RedisCommand.ZPOPMIN.name} $_key 0")
        val r3 = readStringArray()
        assertTrue { r3.isEmpty() }
    }

    @Test
    fun testZrandmember() {
        setup()
        writeCommand("${RedisCommand.ZRANDMEMBER.name} $_key")
        val r = readFullBulkString()
        assertTrue { r.startsWith("val") }

        writeCommand("${RedisCommand.ZRANDMEMBER.name} $_key 5")
        val r2 = readStringArray()
        assertTrue { r2.size == 3 }

        writeCommand("${RedisCommand.ZRANDMEMBER.name} $_key -5")
        val r3 = readStringArray()
        assertTrue { r3.size == 5 }

        writeCommand("${RedisCommand.ZRANDMEMBER.name} $_key 5 WITHSCORES")
        val r4 = readStringArray()
        assertTrue { r4.size == 6 }

        writeCommand("${RedisCommand.ZRANDMEMBER.name} $_key -5 withscores")
        val r5 = readStringArray()
        assertTrue { r5.size == 10 }

        writeCommand("${RedisCommand.ZRANDMEMBER.name} ne")
        assertEquals(nullString, readFullBulkString())
    }

    @Test
    fun testZcount() {
        setup(6)
        writeCommand("${RedisCommand.ZCOUNT.name} $_key -inf +inf")
        assertEquals(6, readLong())
        writeCommand("${RedisCommand.ZCOUNT.name} $_key 2 (5")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.ZCOUNT.name} $_key (1 4")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.ZCOUNT.name} ne 1 2")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.ZCOUNT.name} $_key a b")
        assertTrue { readError().isNotEmpty() }
    }

    @Test
    fun testZlexcount() {
        writeCommand("${RedisCommand.ZADD.name} $_key 0 a 0 b 0 c 0 d 0 e 0 f 0 g")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} $_key - +")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} $_key b f")
        assertEquals(5, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} $_key [c e")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} $_key a [c")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} $_key - [d")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.ZLEXCOUNT.name} ne 1 2")
        assertEquals(0, readLong())
    }

    @Test
    fun testZremrangebylex() {
        writeCommand("${RedisCommand.ZADD.name} $_key 0 a 0 b 0 c 0 d 0 e 0 f 0 g")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZREMRANGEBYLEX.name} $_key - +")
        assertEquals(7, readLong())

        writeCommand("${RedisCommand.ZADD.name} $_key 0 a 0 b <KEY>")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZREMRANGEBYLEX.name} $_key b f")
        assertEquals(5, readLong())

        writeCommand("${RedisCommand.ZADD.name} $_key 0 <KEY>")
        assertEquals(5, readLong())
        writeCommand("${RedisCommand.ZREMRANGEBYLEX.name} $_key [c e")
        assertEquals(2, readLong())

        writeCommand("${RedisCommand.ZADD.name} $_key 0 <KEY>")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.ZREMRANGEBYLEX.name} $_key a [c")
        assertEquals(2, readLong())

        writeCommand("${RedisCommand.ZREMRANGEBYLEX.name} ne 1 2")
        assertEquals(0, readLong())
    }

    @Test
    fun testZremrangebyscore() {
        setup(6)
        writeCommand("${RedisCommand.ZREMRANGEBYSCORE.name} $_key -inf +inf")
        assertEquals(6, readLong())
        writeCommand("${RedisCommand.ZCARD.name} $_key")
        assertEquals(0, readLong())

        clear()
        setup(6)
        writeCommand("${RedisCommand.ZREMRANGEBYSCORE.name} $_key 2 (5")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.ZCARD.name} $_key")
        assertEquals(3, readLong())

        clear()
        setup(6)
        writeCommand("${RedisCommand.ZREMRANGEBYSCORE.name} $_key (1 4")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.ZCARD.name} $_key")
        assertEquals(3, readLong())

        writeCommand("${RedisCommand.ZREMRANGEBYSCORE.name} ne 1 2")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.ZREMRANGEBYSCORE.name} $_key a b")
        assertTrue { readError().isNotEmpty() }
    }

    @Test
    fun testZremrangebyrank() {
        setup()
        setup(3, _key, "v")
        writeCommand("${RedisCommand.ZREMRANGEBYRANK.name} $_key 3 4")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.ZRANGE.name} $_key -inf +inf BYSCORE")
        val r = readStringArray()
        assertTrue { r.size == 4 }
        assertEquals("v1", r[0])
        assertEquals("val1", r[1])
        assertEquals("v2", r[2])
        assertEquals("val3", r[3])

        clear()
        setup()
        setup(3, _key, "v")
        writeCommand("${RedisCommand.ZREMRANGEBYRANK.name} $_key 3 -2")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.ZRANGE.name} $_key -inf +inf BYSCORE")
        val r2 = readStringArray()
        assertTrue { r2.size == 4 }
        assertEquals("v1", r2[0])
        assertEquals("val1", r2[1])
        assertEquals("v2", r2[2])
        assertEquals("val3", r2[3])

        writeCommand("${RedisCommand.ZREMRANGEBYRANK.name} $_key 3 -4")
        assertEquals(0, readLong())

        writeCommand("${RedisCommand.ZREMRANGEBYRANK.name} ne 3 4")
        assertEquals(0, readLong())
    }

    @Test
    fun testZrange() {
        setup(6)
        writeCommand("${RedisCommand.ZRANGE.name} $_key -inf +inf WITHSCORES")
        assertEquals(12, readStringArray().size)

        writeCommand("${RedisCommand.ZRANGE.name} $_key (2 3 WITHSCORES")
        val r = readStringArray()
        assertTrue { r.size == 2 }
        assertEquals("val4", r[0])
        assertEquals("4", r[1])

        writeCommand("${RedisCommand.ZRANGE.name} $_key (2 4 BYSCORE")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val3", r2[0])
        assertEquals("val4", r2[1])

        writeCommand("${RedisCommand.ZRANGE.name} $_key 3 (6 BYSCORE REV LIMIT 1 2")
        val r3 = readStringArray()
        assertTrue { r3.size == 2 }
        assertEquals("val4", r3[0])
        assertEquals("val3", r3[1])
    }

    @Test
    fun testZrangestore() {
        setup(6)
        writeCommand("${RedisCommand.ZRANGESTORE.name} zset2 $_key (2 3")
        assertEquals(1, readLong())

        writeCommand("${RedisCommand.ZRANGE.name} zset2 -inf +inf WITHSCORES")
        val r = readStringArray()
        assertTrue { r.size == 2 }
        assertEquals("val4", r[0])
        assertEquals("4", r[1])

        writeCommand("${RedisCommand.ZRANGESTORE.name} zset3 $_key (2 3 BYSCORE")
        assertEquals(1, readLong())

        writeCommand("${RedisCommand.ZRANGE.name} zset3 -inf +inf WITHSCORES")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val3", r2[0])
        assertEquals("3", r2[1])
    }

    @Test
    fun testZrevrange() {
        setup(6)
        writeCommand("${RedisCommand.ZREVRANGE.name} $_key -inf +inf WITHSCORES")
        assertEquals(12, readStringArray().size)

        writeCommand("${RedisCommand.ZREVRANGE.name} $_key (2 4 WITHSCORES")
        val r = readStringArray()
        assertTrue { r.size == 4 }
        assertEquals("val5", r[0])
        assertEquals("5", r[1])
        assertEquals("val4", r[2])
        assertEquals("4", r[3])

        writeCommand("${RedisCommand.ZREVRANGE.name} $_key (2 4")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val5", r2[0])
        assertEquals("val4", r2[1])
    }

    @Test
    fun testZrangebyscore() {
        setup(6)
        writeCommand("${RedisCommand.ZRANGEBYSCORE.name} $_key -inf +inf WITHSCORES")
        assertEquals(12, readStringArray().size)

        writeCommand("${RedisCommand.ZRANGEBYSCORE.name} $_key (2 4 WITHSCORES")
        val r = readStringArray()
        assertTrue { r.size == 4 }
        assertEquals("val3", r[0])
        assertEquals("3", r[1])
        assertEquals("val4", r[2])
        assertEquals("4", r[3])

        writeCommand("${RedisCommand.ZRANGEBYSCORE.name} $_key (2 4")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val3", r2[0])
        assertEquals("val4", r2[1])
    }

    @Test
    fun testZrevrangebyscore() {
        setup(6)
        writeCommand("${RedisCommand.ZREVRANGEBYSCORE.name} $_key -inf +inf WITHSCORES")
        assertEquals(12, readStringArray().size)

        writeCommand("${RedisCommand.ZREVRANGEBYSCORE.name} $_key (2 4 WITHSCORES")
        val r = readStringArray()
        assertTrue { r.size == 4 }
        assertEquals("val4", r[0])
        assertEquals("4", r[1])
        assertEquals("val3", r[2])
        assertEquals("3", r[3])

        writeCommand("${RedisCommand.ZREVRANGEBYSCORE.name} $_key (2 4")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("val4", r2[0])
        assertEquals("val3", r2[1])
    }

    @Test
    fun testZrangebylex() {
        writeCommand("${RedisCommand.ZADD.name} $_key 0 a 0 b 0 c 0 d 0 e 0 f 0 g")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZRANGEBYLEX.name} $_key - + LIMIT 1 2")
        assertEquals(2, readStringArray().size)

        writeCommand("${RedisCommand.ZRANGEBYLEX.name} $_key [a d")
        val r = readStringArray()
        assertTrue { r.size == 3 }
        assertEquals("b", r[0])
        assertEquals("c", r[1])
        assertEquals("d", r[2])

        writeCommand("${RedisCommand.ZRANGEBYLEX.name} $_key c [e")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("c", r2[0])
        assertEquals("d", r2[1])
    }

    @Test
    fun testZrevrangebylex() {
        writeCommand("${RedisCommand.ZADD.name} $_key 0 a 0 b 0 c 0 d 0 e 0 f 0 g")
        assertEquals(7, readLong())
        writeCommand("${RedisCommand.ZREVRANGEBYLEX.name} $_key - +")
        assertEquals(7, readStringArray().size)

        writeCommand("${RedisCommand.ZREVRANGEBYLEX.name} $_key [a d")
        val r = readStringArray()
        assertTrue { r.size == 3 }
        assertEquals("d", r[0])
        assertEquals("c", r[1])
        assertEquals("b", r[2])

        writeCommand("${RedisCommand.ZREVRANGEBYLEX.name} $_key c [e")
        val r2 = readStringArray()
        assertTrue { r2.size == 2 }
        assertEquals("d", r2[0])
        assertEquals("c", r2[1])
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/scan/KeysCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.scan

import com.aerospike.client.Key
import com.aerospike.client.Record
import com.aerospike.client.ScanCallback
import com.aerospike.client.exp.Exp
import com.aerospike.client.policy.ScanPolicy
import com.aerospike.client.query.KeyRecord
import com.aerospike.client.query.RegexFlag
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import com.aerospike.skyhook.util.RegexUtils
import io.netty.channel.ChannelHandlerContext

class KeysCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx) {

    private lateinit var regexString: String
    private val recordSet: RecordSet by lazy {
        RecordSet()
    }

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount == 2) { argValidationErrorMsg(cmd) }
        regexString = RegexUtils.format(String(cmd.args[1]))

        scan()
        writeScanResponse()
    }

    private fun scan() {
        client.scanAll(buildScanPolicy(), aeroCtx.namespace, aeroCtx.set, callback)
    }

    private fun buildScanPolicy(): ScanPolicy {
        val scanPolicy = ScanPolicy()
        scanPolicy.sendKey = true
        scanPolicy.includeBinData = false

        scanPolicy.filterExp = Exp.build(
            Exp.regexCompare(
                regexString,
                RegexFlag.ICASE or RegexFlag.NEWLINE,
                Exp.key(Exp.Type.STRING)
            )
        )
        return scanPolicy
    }

    private fun writeScanResponse() {
        writeObjectListStr(recordSet.map { it.key.userKey.`object` as String })
        flushCtxTransactionAware()
    }

    private val callback = ScanCallback { key: Key?, record: Record? ->
        recordSet.add(KeyRecord(key, record))
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/SkyhookServer.kt<|end_filename|>
package com.aerospike.skyhook

import com.aerospike.skyhook.config.ServerConfiguration
import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer
import com.aerospike.skyhook.util.SystemUtils
import com.google.inject.name.Named
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
import io.netty.channel.epoll.EpollServerDomainSocketChannel
import io.netty.channel.kqueue.KQueueServerDomainSocketChannel
import io.netty.channel.socket.ServerSocketChannel
import io.netty.channel.unix.DomainSocketAddress
import mu.KotlinLogging
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SkyhookServer @Inject constructor(

    /**
     * Connect server configuration.
     */
    private val config: ServerConfiguration,

    /**
     * Initialize channels.
     */
    private val channelInitializer: AerospikeChannelInitializer,

    /**
     * The event loop group to handle connection requests.
     */
    @Named(NETTY_BOSS_GROUP)
    private val bossGroup: EventLoopGroup,

    /**
     * The event loop group to read and parse incoming requests and write
     * responses.
     */
    @Named(NETTY_WORKER_GROUP)
    private val workerGroup: EventLoopGroup,

    /**
     * The ServerSocketChannel.
     */
    private val socketChannel: ServerSocketChannel
) : Server {

    companion object {
        /**
         * Annotation to get hold of the event loop boss group to handle
         * connection requests.
         */
        const val NETTY_BOSS_GROUP =
            "com.aerospike.skyhook.SkyhookServer." +
                    "NETTY_BOSS_GROUP"

        /**
         * Annotation to get hold of the event loop worker group to read, parse
         * incoming requests.
         */
        const val NETTY_WORKER_GROUP =
            "com.aerospike.skyhook.SkyhookServer." +
                    "NETTY_WORKER_GROUP"

        private val log = KotlinLogging.logger(this::class.java.name)
    }

    /**
     * Is the server started.
     */
    private var started: Boolean = false

    override fun start() {
        if (started) {
            return
        }

        log.info { "Starting the Server..." }

        if (config.unixSocket == null) {
            bindInetSocket()
        } else {
            bindUnixSocket()
        }

        log.info { "Started Netty server with config $config" }
        started = true
    }

    override fun stop() {
        if (!started) {
            return
        }

        log.info("Shutting down Netty event loops...")

        bossGroup.shutdownGracefully().sync()
        workerGroup.shutdownGracefully().sync()
        started = false
    }

    private fun bindInetSocket() {
        val server = initServerBootstrap()

        server.channel(socketChannel.javaClass)
            .childOption(ChannelOption.SO_KEEPALIVE, true)
            .childOption(ChannelOption.TCP_NODELAY, true)
            .bind(config.redisPort).sync()
    }

    private fun bindUnixSocket() {
        val server = initServerBootstrap()

        val channel = when (SystemUtils.os) {
            SystemUtils.OS.LINUX -> EpollServerDomainSocketChannel()
            SystemUtils.OS.MAC -> KQueueServerDomainSocketChannel()
            else -> throw IllegalArgumentException("Unsupported UNIX Socket")
        }

        server.channel(channel.javaClass)
            .bind(DomainSocketAddress(config.unixSocket)).sync()
    }

    private fun initServerBootstrap(): ServerBootstrap {
        val server = ServerBootstrap()

        server.group(bossGroup, workerGroup)
            .childHandler(channelInitializer)
            .option(ChannelOption.SO_BACKLOG, 128)
        return server
    }

}


<|start_filename|>src/test/kotlin/com/aerospike/skyhook/ScanCommandsTest.kt<|end_filename|>
package com.aerospike.skyhook

import com.aerospike.skyhook.command.RedisCommand
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class ScanCommandsTest() : SkyhookIntegrationTestBase() {

    @Test
    fun testScan() {
        writeCommand("${RedisCommand.MSET.name} k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k11 v11 k8 v8")
        assertEquals(ok, readString())
        writeCommand("${RedisCommand.SADD.name} set a b c")
        assertEquals(3L, readLong())

        writeCommand("${RedisCommand.SCAN.name} 0 COUNT 20 TYPE string MATCH k1*")
        Thread.sleep(5000)
        val resp = readScanResponse()
        assertEquals("0", resp.cursor)
        assertEquals(2, resp.elements.size)
        assertEquals("k11", resp.elements[0])
        assertEquals("k1", resp.elements[1])
    }

    @Test
    fun testHscan() {
        writeCommand("${RedisCommand.HSET.name} hash k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8 k9 v9")
        assertEquals(9L, readLong())

        writeCommand("${RedisCommand.HSCAN.name} hash 0 COUNT 5")
        var resp = readScanResponse()
        assertEquals("5", resp.cursor)
        assertEquals(10, resp.elements.size)

        writeCommand("${RedisCommand.HSCAN.name} hash ${resp.cursor} COUNT 5")
        resp = readScanResponse()
        assertEquals("0", resp.cursor)
        assertEquals(8, resp.elements.size)
    }

    @Test
    fun testSscan() {
        writeCommand("${RedisCommand.SADD.name} set a b c d e f g h i")
        assertEquals(9L, readLong())

        writeCommand("${RedisCommand.SSCAN.name} set 0 COUNT 5")
        var resp = readScanResponse()
        assertEquals("5", resp.cursor)
        assertEquals(5, resp.elements.size)

        writeCommand("${RedisCommand.SSCAN.name} set ${resp.cursor} COUNT 5")
        resp = readScanResponse()
        assertEquals("0", resp.cursor)
        assertEquals(4, resp.elements.size)
    }

    @Test
    fun testZscan() {
        writeCommand("${RedisCommand.ZADD.name} zset 0 a 2 b 1 c 4 d 3 e 6 f 5 g 8 h 7 i")
        assertEquals(9L, readLong())

        writeCommand("${RedisCommand.ZSCAN.name} zset 0 COUNT 5")
        var resp = readScanResponse()
        assertEquals("5", resp.cursor)
        assertEquals(5, resp.elements.size)
        assertEquals("a", resp.elements[0])
        assertEquals("c", resp.elements[1])
        assertEquals("b", resp.elements[2])
        assertEquals("e", resp.elements[3])
        assertEquals("d", resp.elements[4])

        writeCommand("${RedisCommand.ZSCAN.name} zset ${resp.cursor} COUNT 5")
        resp = readScanResponse()
        assertEquals("0", resp.cursor)
        assertEquals(4, resp.elements.size)
        assertEquals("g", resp.elements[0])
        assertEquals("f", resp.elements[1])
        assertEquals("i", resp.elements[2])
        assertEquals("h", resp.elements[3])
    }

    @Test
    fun testKeys() {
        writeCommand("${RedisCommand.MSET.name} k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k11 v11 k8 v8")
        assertEquals(ok, readString())

        writeCommand("${RedisCommand.KEYS.name} k1*")
        Thread.sleep(3000)
        val r = readStringArray()
        assertEquals("k11", r[0])
        assertEquals("k1", r[1])
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/map/ZaddCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.map

import com.aerospike.client.Key
import com.aerospike.client.Operation
import com.aerospike.client.Record
import com.aerospike.client.Value
import com.aerospike.client.cdt.MapOperation
import com.aerospike.client.cdt.MapOrder
import com.aerospike.client.cdt.MapPolicy
import com.aerospike.client.cdt.MapWriteFlags
import com.aerospike.client.listener.RecordListener
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import com.aerospike.skyhook.listener.ValueType
import com.aerospike.skyhook.util.Typed
import io.netty.channel.ChannelHandlerContext
import java.util.*

class ZaddCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx), RecordListener {

    private class ZaddCommand(val cmd: RequestCommand) {
        var XX: Boolean = false
            private set
        var NX: Boolean = false
            private set
        var LT: Boolean = false
            private set
        var GT: Boolean = false
            private set
        var CH: Boolean = false
            private set
        var INCR: Boolean = false
            private set

        lateinit var values: Map<Value, Value>
            private set

        init {
            for (i in 2 until cmd.args.size) {
                if (!setFlag(String(cmd.args[i]))) {
                    setSortedSetValues(i)
                    break
                }
            }
            validate()
        }

        private fun setFlag(flagStr: String): Boolean {
            when (flagStr.uppercase(Locale.ENGLISH)) {
                "XX" -> XX = true
                "NX" -> NX = true
                "LT" -> LT = true
                "GT" -> GT = true
                "CH" -> CH = true
                "INCR" -> INCR = true
                else -> return false
            }
            return true
        }

        private fun setSortedSetValues(from: Int) {
            values = cmd.args.drop(from).chunked(2)
                .map { (it1, it2) ->
                    Typed.getStringValue(it2) to
                            Value.LongValue(Typed.getLong(it1))
                }
                .toMap()
        }

        private fun validate() {
            require(!(NX && XX)) { "[NX|XX]" }
            require(!(GT && LT)) { "[GT|LT]" }
            require(!LT) { "LT flag not supported" }
            require(!GT) { "GT flag not supported" }
            require(!CH) { "CH flag not supported" }
        }
    }

    @Volatile
    private var size: Long = 0L

    @Volatile
    private lateinit var zaddCommand: ZaddCommand

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount >= 4) { argValidationErrorMsg(cmd) }

        val key = createKey(cmd.key)
        zaddCommand = ZaddCommand(cmd)

        val getSize = MapOperation.size(aeroCtx.bin)
        size = client.operate(defaultWritePolicy, key, getSize)
            ?.getLong(aeroCtx.bin) ?: 0L

        client.operate(
            null, this, defaultWritePolicy,
            key, *systemOps(ValueType.ZSET), getMapOperation()
        )
    }

    private fun getMapOperation(): Operation {
        return when {
            zaddCommand.INCR -> {
                require(zaddCommand.values.size == 1) { "INCR params" }
                MapOperation.increment(
                    getMapPolicy(),
                    aeroCtx.bin,
                    zaddCommand.values.keys.first(),
                    zaddCommand.values.values.first()
                )
            }
            else -> {
                MapOperation.putItems(
                    getMapPolicy(),
                    aeroCtx.bin,
                    zaddCommand.values
                )
            }
        }
    }

    private fun getMapPolicy(): MapPolicy {
        val writeFlag = when {
            zaddCommand.XX -> {
                MapWriteFlags.UPDATE_ONLY
            }
            zaddCommand.NX -> {
                MapWriteFlags.CREATE_ONLY
            }
            else -> {
                MapWriteFlags.DEFAULT
            }
        }
        return MapPolicy(MapOrder.KEY_VALUE_ORDERED, writeFlag)
    }

    override fun onSuccess(key: Key?, record: Record?) {
        if (record == null) {
            writeLong(0L)
            flushCtxTransactionAware()
        } else {
            try {
                if (zaddCommand.INCR) {
                    writeResponse(record.getString(aeroCtx.bin))
                } else {
                    val added = record.getLong(aeroCtx.bin) - size
                    writeLong(added)
                }
                flushCtxTransactionAware()
            } catch (e: Exception) {
                closeCtx(e)
            }
        }
    }
}


<|start_filename|>src/test/kotlin/com/aerospike/skyhook/HashCommandsTest.kt<|end_filename|>
package com.aerospike.skyhook

import com.aerospike.skyhook.command.RedisCommand
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class HashCommandsTest() : SkyhookIntegrationTestBase() {

    private val _key = "hash"

    private fun setup(n: Int = 3) {
        for (i in 1..n) {
            writeCommand("${RedisCommand.HSET.name} $_key key$i val$i")
            assertEquals(1, readLong())
        }
    }

    @Test
    fun testHset() {
        writeCommand("${RedisCommand.HSET.name} $_key key1 val1 key2 val2")
        assertEquals(2, readLong())
        writeCommand("${RedisCommand.HSET.name} $_key key1 val11 key3 val3")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.HGET.name} $_key key1")
        assertEquals("val11", readFullBulkString())
        writeCommand("${RedisCommand.HGET.name} $_key key2")
        assertEquals("val2", readFullBulkString())
        writeCommand("${RedisCommand.HGET.name} $_key key3")
        assertEquals("val3", readFullBulkString())
    }

    @Test
    fun testHget() {
        setup()
        writeCommand("${RedisCommand.HGET.name} $_key key1")
        assertEquals("val1", readFullBulkString())
        writeCommand("${RedisCommand.HGET.name} $_key key11")
        assertEquals(nullString, readFullBulkString())
        writeCommand("${RedisCommand.HGET.name} ne key1")
        assertEquals(nullString, readFullBulkString())
    }

    @Test
    fun testHsetnx() {
        writeCommand("${RedisCommand.HSETNX.name} $_key key1 val1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.HSETNX.name} $_key key1 val11")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.HGET.name} $_key key1")
        assertEquals("val1", readFullBulkString())
    }

    @Test
    fun testHmset() {
        writeCommand("${RedisCommand.HMSET.name} $_key key1 val1 key2 val2")
        assertEquals(ok, readString())
        writeCommand("${RedisCommand.HMSET.name} $_key key1 val11 key2 val2")
        assertEquals(ok, readString())
        writeCommand("${RedisCommand.HGET.name} $_key key1")
        assertEquals("val11", readFullBulkString())
        writeCommand("${RedisCommand.HGET.name} $_key key2")
        assertEquals("val2", readFullBulkString())
    }

    @Test
    fun testHexists() {
        setup(1)
        writeCommand("${RedisCommand.HEXISTS.name} $_key key1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.HEXISTS.name} $_key key2")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.HEXISTS.name} ne key1")
        assertEquals(0, readLong())
    }

    @Test
    fun testHmget() {
        setup()
        writeCommand("${RedisCommand.HMGET.name} $_key key1 key2 key3")
        val r = readStringArray()
        assertEquals("val1", r[0])
        assertEquals("val2", r[1])
        assertEquals("val3", r[2])
    }

    @Test
    fun testHgetall() {
        setup()
        writeCommand("${RedisCommand.HGETALL.name} $_key")
        val r = readStringArray()
        assertEquals("key1", r[0])
        assertEquals("val1", r[1])
        assertEquals("key2", r[2])
        assertEquals("val2", r[3])
        assertEquals("key3", r[4])
        assertEquals("val3", r[5])
        writeCommand("${RedisCommand.HGETALL.name} ne")
        val r2 = readStringArray()
        assertTrue { r2.isEmpty() }
    }

    @Test
    fun testHkeys() {
        setup()
        writeCommand("${RedisCommand.HKEYS.name} $_key")
        val r = readStringArray()
        assertEquals("key1", r[0])
        assertEquals("key2", r[1])
        assertEquals("key3", r[2])
        writeCommand("${RedisCommand.HKEYS.name} ne")
        val r2 = readStringArray()
        assertTrue { r2.isEmpty() }
    }

    @Test
    fun testHvals() {
        setup()
        writeCommand("${RedisCommand.HVALS.name} $_key")
        val r = readStringArray()
        assertEquals("val1", r[0])
        assertEquals("val2", r[1])
        assertEquals("val3", r[2])
        writeCommand("${RedisCommand.HVALS.name} ne")
        val r2 = readStringArray()
        assertTrue { r2.isEmpty() }
    }

    @Test
    fun testHincrby() {
        writeCommand("${RedisCommand.HSET.name} $_key key1 10")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.HINCRBY.name} $_key key1 5")
        assertEquals(15, readLong())
    }

    @Test
    fun testHstrlen() {
        setup(1)
        writeCommand("${RedisCommand.HSTRLEN.name} $_key key1")
        assertEquals(4, readLong())
        writeCommand("${RedisCommand.HSTRLEN.name} $_key key2")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.HSTRLEN.name} hash2 key1")
        assertEquals(0, readLong())
    }

    @Test
    fun testHlen() {
        setup()
        writeCommand("${RedisCommand.HLEN.name} $_key")
        assertEquals(3, readLong())
        writeCommand("${RedisCommand.HLEN.name} hash2")
        assertEquals(0, readLong())
    }

    @Test
    fun testHdel() {
        setup(1)
        writeCommand("${RedisCommand.HDEL.name} $_key key1")
        assertEquals(1, readLong())
        writeCommand("${RedisCommand.HDEL.name} $_key key1")
        assertEquals(0, readLong())
        writeCommand("${RedisCommand.HGET.name} $_key key1")
        assertEquals(nullString, readFullBulkString())
    }

    @Test
    fun testHrandfield() {
        setup()
        writeCommand("${RedisCommand.HRANDFIELD.name} $_key")
        val r = readFullBulkString()
        assertTrue { r.startsWith("key") }

        writeCommand("${RedisCommand.HRANDFIELD.name} $_key 5")
        val r2 = readStringArray()
        assertTrue { r2.size == 3 }

        writeCommand("${RedisCommand.HRANDFIELD.name} $_key -5")
        val r3 = readStringArray()
        assertTrue { r3.size == 5 }

        writeCommand("${RedisCommand.HRANDFIELD.name} $_key 5 WITHVALUES")
        val r4 = readStringArray()
        assertTrue { r4.size == 6 }

        writeCommand("${RedisCommand.HRANDFIELD.name} $_key -5 withvalues")
        val r5 = readStringArray()
        assertTrue { r5.size == 10 }

        writeCommand("${RedisCommand.HRANDFIELD.name} ne")
        assertEquals(nullString, readFullBulkString())
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/util/InfoUtils.kt<|end_filename|>
package com.aerospike.skyhook.util

import com.aerospike.client.Info
import com.aerospike.client.cluster.Node
import java.util.regex.Pattern

object InfoUtils {

    fun getNamespaceInfo(ns: String, node: Node): Map<String, String> {
        val schemaInfo = Info.request(null, node, "namespace/$ns")
        return schemaInfo.split(";")
            .associate { it.split("=".toRegex(), 2).toPair() }
    }

    fun getSetInfo(ns: String, set: String?, node: Node): Map<String, String> {
        val sets = Info.request(null, node, "sets")
        val tableInfo = sets.split(";")
            .filter { it.startsWith("ns=$ns:set=$set") }[0]
        return Pattern.compile("\\s*:\\s*").split(tableInfo).toList()
            .filterNotNull().associate { it.split("=".toRegex(), 2).toPair() }
    }
}


<|start_filename|>build.gradle.kts<|end_filename|>
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    java
    application
    kotlin("jvm") version "1.6.0"
    id("com.github.johnrengelman.shadow") version "7.1.0"
    id("nebula.ospackage") version "8.6.3"
}

group = "com.aerospike"

repositories {
    mavenCentral()
}

application {
    mainClass.set("com.aerospike.skyhook.MainKt")
}

tasks.withType<Jar> {
    manifest {
        attributes(
            mapOf(
                "Main-Class" to application.mainClass,
                "Implementation-Version" to archiveVersion
            )
        )
    }
}

// Common dependency versions.
extra["nettyVersion"] = "4.1.72.Final"
extra["logbackVersion"] = "1.2.10"
extra["jacksonVersion"] = "2.13.1"

dependencies {
    implementation("com.aerospike:aerospike-client:5.1.11")
    implementation("io.netty:netty-all:${project.extra["nettyVersion"]}")
    implementation("io.netty:netty-codec-redis:${project.extra["nettyVersion"]}")
    implementation("com.google.inject:guice:5.0.1")
    implementation("io.github.microutils:kotlin-logging:2.1.21")
    implementation("ch.qos.logback:logback-classic:${project.extra["logbackVersion"]}")
    implementation("ch.qos.logback:logback-core:${project.extra["logbackVersion"]}")
    implementation("info.picocli:picocli:4.6.2")
    implementation("commons-io:commons-io:2.11.0")
    implementation("com.fasterxml.jackson.core:jackson-core:${project.extra["jacksonVersion"]}")
    implementation("com.fasterxml.jackson.core:jackson-annotations:${project.extra["jacksonVersion"]}")
    implementation("com.fasterxml.jackson.core:jackson-databind:${project.extra["jacksonVersion"]}")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${project.extra["jacksonVersion"]}")
    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${project.extra["jacksonVersion"]}")
    implementation("com.google.guava:guava:31.0.1-jre")

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

val distZip: Zip by tasks
distZip.duplicatesStrategy = DuplicatesStrategy.EXCLUDE

fun setUpPackaging(
    packagingTask: com.netflix.gradle.plugins.packaging.SystemPackagingTask,
    installFolder: String = "install",
    debFolder: String = "deb"
) {
    packagingTask.dependsOn("distZip")

    // Mark configuration file as configuration file.
    val targetConfigFile =
        "/etc/${packagingTask.project.name}/${packagingTask.project.name}.yml"
    packagingTask.configurationFile(targetConfigFile)

    val sourceConfigFile =
        File("${projectDir}/pkg/$installFolder/$targetConfigFile")

    if (packagingTask is com.netflix.gradle.plugins.rpm.Rpm && sourceConfigFile.isFile) {
        packagingTask.from(sourceConfigFile) {
            // For rpm this is the way to mark configuration files.
            com.netflix.gradle.plugins.packaging.CopySpecEnhancement
                .setFileType(this,
                    org.redline_rpm.payload.Directive(
                        org.redline_rpm.payload.Directive.RPMFILE_CONFIG or org.redline_rpm.payload.Directive
                            .RPMFILE_NOREPLACE
                    )
                )
            into(
                sourceConfigFile.parentFile.path.replace(
                    ".*pkg/$installFolder".toRegex(), ""))
        }
    }

    // Package config and other dependent files.
    val installDir = File(project.projectDir, "pkg/$installFolder")
    if (installDir.isDirectory) {
        for (file in installDir.listFiles()!!) {
            packagingTask.from(file) {
                into(
                    file.path.replace(".*pkg/$installFolder".toRegex(), ""))
                packagingTask.addParentDirs = false
                fileMode = 0x755
                if (packagingTask is com.netflix.gradle.plugins.rpm.Rpm) {
                    // We have config it separately above.
                    exclude("**/" + sourceConfigFile.name)
                }
            }
        }
    }

    // Copy installation scripts.
    val installScriptsDir = File(project.projectDir, "pkg/$debFolder")
    if (installScriptsDir.isDirectory) {
        packagingTask.postInstall(
            packagingTask.project.file(
                "${projectDir}/pkg/$debFolder/postInstall.sh"))
        packagingTask.preUninstall(
            packagingTask.project.file
                ("${projectDir}/pkg/$debFolder/preUninstall.sh"))
        packagingTask.postUninstall(
            packagingTask.project.file(
                "${projectDir}/pkg/$debFolder/postUninstall.sh"))
    }

    // Copy the installer.
    packagingTask.from(project.zipTree(
        distZip.archiveFile.get().asFile.absolutePath)) {
        into("/opt/${packagingTask.project.name}/")
        packagingTask.addParentDirs = false
        eachFile {
            path =
                path.replaceFirst(
                    "/${packagingTask.project.name}-${packagingTask.project.version}/",
                    "/"
                )
        }
    }
}

/**
 * Create the Debian package.
 */
task("deb", com.netflix.gradle.plugins.deb.Deb::class) {
    // Currently we cannot specify a dependency on any package providing
    // java 8+ (for e.g java 11). Exact dependency on Java 8 is the only
    // thing that works. Skip adding a packaging dependency until this
    // works reliably.
    // requires("java8-runtime").or("java8-sdk")
    setUpPackaging(this)
}

/**
 * Create the RPM package.
 */
task("rpm", com.netflix.gradle.plugins.rpm.Rpm::class) {
    os = org.redline_rpm.header.Os.LINUX
    release = "1"
    user = "root"
    packageGroup = "root"

    // Currently we cannot specify a dependency on any package providing
    // java 8+ (for e.g java 11). Exact dependency on Java 8 is the only
    // thing that works. Skip adding a packaging dependency until this
    // works reliably.
    // requires("java", "1.8", Flags.GREATER or Flags.EQUAL).or("java",
    //    "11", Flags.GREATER or Flags.EQUAL)
    setUpPackaging(this)
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/map/RandmemberCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.map

import com.aerospike.client.Key
import com.aerospike.client.Operation
import com.aerospike.client.Record
import com.aerospike.client.cdt.MapOperation
import com.aerospike.client.cdt.MapReturnType
import com.aerospike.client.listener.RecordListener
import com.aerospike.skyhook.command.RedisCommand
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import io.netty.channel.ChannelHandlerContext
import java.util.concurrent.atomic.AtomicInteger

class RandmemberCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx), RecordListener {

    @Volatile
    private lateinit var randCommand: RandCommand

    private class RandCommand(val cmd: RequestCommand) {

        private val total: AtomicInteger = AtomicInteger()

        val count by lazy {
            if (cmd.argCount == 2) 1 else String(cmd.args[2]).toInt()
        }

        val withModifier by lazy {
            if (cmd.argCount < 4) {
                false
            } else {
                validateWith()
                true
            }
        }

        private fun validateWith() {
            val with = String(cmd.args[3])
            require(
                (cmd.command == RedisCommand.ZRANDMEMBER && with.equals("WITHSCORES", true)) ||
                        (cmd.command == RedisCommand.HRANDFIELD && with.equals("WITHVALUES", true))
            ) {
                argValidationErrorMsg(cmd)
            }
        }

        val step by lazy {
            if (withModifier) 2 else 1
        }

        fun set(size: Int) {
            total.set(size * step)
        }

        fun get(): Int {
            return total.get()
        }

        fun decrementAndGet(): Int {
            return total.addAndGet(-step)
        }
    }

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount >= 2 || cmd.argCount <= 4) {
            argValidationErrorMsg(cmd)
        }

        randCommand = RandCommand(cmd)
        val key = createKey(cmd.key)
        val indexList = getRandomList(key)
        if (indexList == null) {
            writeNullString()
            flushCtxTransactionAware()
            return
        }

        randCommand.set(indexList.size)
        if (randCommand.get() > 1) {
            writeArrayHeader(randCommand.get().toLong())
        }
        for (i: Int in indexList) {
            client.operate(
                null, this, defaultWritePolicy,
                key, getMapOperation(i)
            )
        }
    }

    private fun getRandomList(key: Key): List<Int>? {
        val setSize = getSetSize(key) ?: return null
        return if (randCommand.count >= 0) {
            val count = minOf(randCommand.count, setSize)
            val mutableSet: MutableSet<Int> = mutableSetOf()
            while (mutableSet.size < count) {
                mutableSet.add((0 until setSize).random())
            }
            mutableSet.toList()
        } else {
            val count = kotlin.math.abs(randCommand.count)
            val mutableList: MutableList<Int> = mutableListOf()
            while (mutableList.size < count) {
                mutableList.add((0 until setSize).random())
            }
            mutableList.toList()
        }
    }

    private fun getMapOperation(i: Int): Operation {
        val returnType = if (randCommand.withModifier) {
            MapReturnType.KEY_VALUE
        } else {
            MapReturnType.KEY
        }
        return MapOperation.getByIndex(aeroCtx.bin, i, returnType)
    }

    private fun getSetSize(key: Key): Int? {
        return client.operate(
            defaultWritePolicy, key,
            MapOperation.size(aeroCtx.bin)
        )?.getInt(aeroCtx.bin)
    }

    override fun onSuccess(key: Key?, record: Record?) {
        try {
            if (record == null) {
                writeNullString()
            } else {
                synchronized(randCommand) {
                    writeResponse(record.bins[aeroCtx.bin])
                }
            }
            if (randCommand.decrementAndGet() == 0) {
                flushCtxTransactionAware()
            }
        } catch (e: Exception) {
            closeCtx(e)
        }
    }

    override fun writeResponse(mapped: Any?) {
        when (mapped) {
            is Map<*, *> -> mapped.toList().forEach {
                super.writeResponse(it.first.toString())
                super.writeResponse(it.second.toString())
            }
            is List<*> -> when (mapped.firstOrNull()) {
                is Map.Entry<*, *> -> mapped.map { it as Map.Entry<*, *> }.map { it.toPair() }
                    .forEach {
                        super.writeResponse(it.first.toString())
                        super.writeResponse(it.second.toString())
                    }
                else -> mapped.forEach {
                    super.writeResponse(it.toString())
                }
            }
            else -> {
                super.writeResponse(mapped)
            }
        }
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/command/RedisCommand.kt<|end_filename|>
package com.aerospike.skyhook.command

import com.aerospike.skyhook.command.CommandsDetails.appendCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.authCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.bgsaveCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.commandCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.dbsizeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.decrCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.decrbyCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.delCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.discardCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.echoCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.execCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.existsCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.expireCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.expireatCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.flushallCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.flushdbCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.getCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.getdelCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.getexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.getsetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hdelCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hexistsCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hgetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hgetallCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hincrbyCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hincrbyfloatCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hkeysCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hlenCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hmgetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hmsetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hrandfieldCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hscanCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hsetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hsetnxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hstrlenCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.hvalsCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.incrCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.incrbyCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.incrbyfloatCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.keysCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lindexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.llenCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lolwutCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lpopCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lpushCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lpushxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.lrangeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.mgetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.msetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.msetnxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.multiCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.persistCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.pexpireCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.pexpireatCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.pingCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.psetexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.pttlCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.randomkeyCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.resetCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.rpopCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.rpushCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.rpushxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.saddCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.saveCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.scanCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.scardCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.setCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.setexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.setnxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sinterCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sinterstoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sismemberCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.smembersCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.smismemberCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.srandmemberCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sremCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sscanCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.strlenCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sunionCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.sunionstoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.timeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.touchCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.ttlCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.typeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.unlinkCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zaddCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zcardCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zcountCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zincrbyCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zlexcountCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zmscoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zpopmaxCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zpopminCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrandmemberCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrangeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrangebylexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrangebyscoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrangestoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrankCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zremCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zremrangebylexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zremrangebyrankCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zremrangebyscoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrevrangeCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrevrangebylexCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zrevrangebyscoreCommandDetails
import com.aerospike.skyhook.command.CommandsDetails.zscanCommandDetails
import com.aerospike.skyhook.handler.CommandHandler
import com.aerospike.skyhook.handler.aerospike.*
import com.aerospike.skyhook.handler.redis.*
import com.aerospike.skyhook.listener.key.*
import com.aerospike.skyhook.listener.list.*
import com.aerospike.skyhook.listener.map.*
import com.aerospike.skyhook.listener.scan.*
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.redis.ArrayHeaderRedisMessage
import mu.KotlinLogging
import java.util.*
import kotlin.reflect.KFunction1

enum class RedisCommand(
    val details: RedisCommandDetails?,
    val newHandler: KFunction1<ChannelHandlerContext, CommandHandler>
) {
    GET(getCommandDetails, ::GetCommandListener),
    GETEX(getexCommandDetails, ::GetexCommandListener),
    MGET(mgetCommandDetails, ::MgetCommandListener),
    GETSET(getsetCommandDetails, ::GetsetCommandListener),
    GETDEL(getdelCommandDetails, ::GetdelCommandListener),
    SET(setCommandDetails, ::SetCommandListener),
    SETEX(setexCommandDetails, ::SetCommandListener),
    PSETEX(psetexCommandDetails, ::SetCommandListener),
    SETNX(setnxCommandDetails, ::SetCommandListener),
    MSET(msetCommandDetails, ::MsetCommandListener),
    MSETNX(msetnxCommandDetails, ::MsetCommandListener),
    EXISTS(existsCommandDetails, ::ExistsCommandListener),
    EXPIRE(expireCommandDetails, ::ExpireCommandListener),
    PEXPIRE(pexpireCommandDetails, ::ExpireCommandListener),
    EXPIREAT(expireatCommandDetails, ::ExpireCommandListener),
    PEXPIREAT(pexpireatCommandDetails, ::ExpireCommandListener),
    PERSIST(persistCommandDetails, ::ExpireCommandListener),
    APPEND(appendCommandDetails, ::AppendCommandListener),
    INCR(incrCommandDetails, ::IncrCommandListener),
    INCRBY(incrbyCommandDetails, ::IncrCommandListener),
    INCRBYFLOAT(incrbyfloatCommandDetails, ::IncrCommandListener),
    DECR(decrCommandDetails, ::DecrCommandListener),
    DECRBY(decrbyCommandDetails, ::DecrCommandListener),
    STRLEN(strlenCommandDetails, ::StrlenCommandListener),
    TTL(ttlCommandDetails, ::TtlCommandListener),
    PTTL(pttlCommandDetails, ::TtlCommandListener),
    DEL(delCommandDetails, ::DelCommandListener),
    UNLINK(unlinkCommandDetails, ::DelCommandListener),
    RANDOMKEY(randomkeyCommandDetails, ::RandomkeyCommandListener),

    /**
     * The Redis TOUCH command changes the last_access_time of the key (used for LRU eviction).
     * Aerospike (currently) doesn't have such metadata on the record.
     * The actual implementation returns the number of records that were 'touched' using
     * [com.aerospike.client.AerospikeClient.exists].
     */
    TOUCH(touchCommandDetails, ::ExistsCommandListener),
    TYPE(typeCommandDetails, ::TypeCommandListener),

    LPUSH(lpushCommandDetails, ::ListPushCommandListener),
    LPUSHX(lpushxCommandDetails, ::ListPushCommandListener),
    RPUSH(rpushCommandDetails, ::ListPushCommandListener),
    RPUSHX(rpushxCommandDetails, ::ListPushCommandListener),
    LINDEX(lindexCommandDetails, ::LindexCommandListener),
    LLEN(llenCommandDetails, ::LlenCommandListener),
    LPOP(lpopCommandDetails, ::ListPopCommandListener),
    RPOP(rpopCommandDetails, ::ListPopCommandListener),
    LRANGE(lrangeCommandDetails, ::LrangeCommandListener),

    HSET(hsetCommandDetails, ::HsetCommandListener),
    HSETNX(hsetnxCommandDetails, ::HsetnxCommandListener),
    HMSET(hmsetCommandDetails, ::HmsetCommandListener),
    SADD(saddCommandDetails, ::SaddCommandListener),
    HEXISTS(hexistsCommandDetails, ::HexistsCommandListener),
    SISMEMBER(sismemberCommandDetails, ::HexistsCommandListener),
    SMISMEMBER(smismemberCommandDetails, ::SmismemberCommandListener),
    HGET(hgetCommandDetails, ::MapGetCommandListener),
    HMGET(hmgetCommandDetails, ::MapGetCommandListener),
    HGETALL(hgetallCommandDetails, ::MapGetCommandListener),
    HVALS(hvalsCommandDetails, ::MapGetCommandListener),
    HKEYS(hkeysCommandDetails, ::MapGetCommandListener),
    ZMSCORE(zmscoreCommandDetails, ::MapGetCommandListener),
    ZRANK(zrankCommandDetails, ::MapGetCommandListener),
    SMEMBERS(smembersCommandDetails, ::MapGetCommandListener),
    HINCRBY(hincrbyCommandDetails, ::HincrbyCommandListener),
    HINCRBYFLOAT(hincrbyfloatCommandDetails, ::HincrbyCommandListener),
    ZINCRBY(zincrbyCommandDetails, ::HincrbyCommandListener),
    HSTRLEN(hstrlenCommandDetails, ::HstrlenCommandListener),
    HLEN(hlenCommandDetails, ::MapSizeCommandListener),
    SCARD(scardCommandDetails, ::MapSizeCommandListener),
    ZCARD(zcardCommandDetails, ::MapSizeCommandListener),
    HDEL(hdelCommandDetails, ::MapDelCommandListener),
    SREM(sremCommandDetails, ::MapDelCommandListener),
    ZREM(zremCommandDetails, ::MapDelCommandListener),
    SUNION(sunionCommandDetails, ::SunionCommandListener),
    SINTER(sinterCommandDetails, ::SinterCommandListener),
    SUNIONSTORE(sunionstoreCommandDetails, ::SunionstoreCommandListener),
    SINTERSTORE(sinterstoreCommandDetails, ::SinterstoreCommandListener),
    ZADD(zaddCommandDetails, ::ZaddCommandListener),
    ZPOPMAX(zpopmaxCommandDetails, ::ZpopmaxCommandListener),
    ZPOPMIN(zpopminCommandDetails, ::ZpopminCommandListener),
    ZRANDMEMBER(zrandmemberCommandDetails, ::RandmemberCommandListener),
    SRANDMEMBER(srandmemberCommandDetails, ::RandmemberCommandListener),
    HRANDFIELD(hrandfieldCommandDetails, ::RandmemberCommandListener),
    ZCOUNT(zcountCommandDetails, ::ZcountCommandListener),
    ZLEXCOUNT(zlexcountCommandDetails, ::ZlexcountCommandListener),
    ZREMRANGEBYSCORE(zremrangebyscoreCommandDetails, ::ZremrangebyscoreCommandListener),
    ZREMRANGEBYRANK(zremrangebyrankCommandDetails, ::ZremrangebyrankCommandListener),
    ZREMRANGEBYLEX(zremrangebylexCommandDetails, ::ZremrangebylexCommandListener),
    ZRANGE(zrangeCommandDetails, ::ZrangeCommandListener),
    ZRANGESTORE(zrangestoreCommandDetails, ::ZrangestoreCommandListener),
    ZREVRANGE(zrevrangeCommandDetails, ::ZrevrangeCommandListener),
    ZRANGEBYSCORE(zrangebyscoreCommandDetails, ::ZrangebyscoreCommandListener),
    ZREVRANGEBYSCORE(zrevrangebyscoreCommandDetails, ::ZrevrangebyscoreCommandListener),
    ZRANGEBYLEX(zrangebylexCommandDetails, ::ZrangebylexCommandListener),
    ZREVRANGEBYLEX(zrevrangebylexCommandDetails, ::ZrevrangebylexCommandListener),

    SCAN(scanCommandDetails, ::ScanCommandListener),
    HSCAN(hscanCommandDetails, ::HscanCommandListener),
    SSCAN(sscanCommandDetails, ::SscanCommandListener),
    ZSCAN(zscanCommandDetails, ::ZscanCommandListener),
    KEYS(keysCommandDetails, ::KeysCommandListener),

    FLUSHDB(flushdbCommandDetails, ::FlushCommandHandler),
    FLUSHALL(flushallCommandDetails, ::FlushCommandHandler),
    DBSIZE(dbsizeCommandDetails, ::DbsizeCommandHandler),

    PING(pingCommandDetails, ::PingCommandHandler),
    ECHO(echoCommandDetails, ::EchoCommandHandler),
    LOLWUT(lolwutCommandDetails, ::LolwutCommandHandler),
    TIME(timeCommandDetails, ::TimeCommandHandler),
    QUIT(null, ::MockCommandHandler),
    RESET(resetCommandDetails, ::MockCommandHandler),
    SAVE(saveCommandDetails, ::MockCommandHandler),
    BGSAVE(bgsaveCommandDetails, ::MockCommandHandler),
    AUTH(authCommandDetails, ::AuthCommandHandler),

    MULTI(multiCommandDetails, ::MultiCommandHandler),
    DISCARD(discardCommandDetails, ::DiscardCommandHandler),
    EXEC(execCommandDetails, ::ExecCommandHandler),

    COMMAND(commandCommandDetails, ::CommandCommandHandler);

    companion object {
        private val log = KotlinLogging.logger {}

        fun getValue(stringValue: String): RedisCommand {
            return try {
                valueOf(stringValue.uppercase(Locale.ENGLISH))
            } catch (e: IllegalArgumentException) {
                val msg = "ERR $stringValue unsupported command"
                log.warn { msg }
                throw UnsupportedOperationException(msg)
            }
        }

        val totalCommands: Long by lazy {
            enumValues<RedisCommand>().filter { it.details != null }.size.toLong()
        }

        fun writeCommand(ctx: ChannelHandlerContext) {
            ctx.write(ArrayHeaderRedisMessage(totalCommands))
            enumValues<RedisCommand>().filter { it.details != null }
                .forEach { it.details!!.write(ctx) }
        }

        fun writeCommandInfo(ctx: ChannelHandlerContext, commands: List<String>) {
            val list = enumValues<RedisCommand>().filter { it.details != null }
                .filter { commands.contains(it.details!!.commandName) }
            ctx.write(ArrayHeaderRedisMessage(list.size.toLong()))
            list.forEach { it.details!!.write(ctx) }
        }
    }
}

object CommandsDetails {

    val getCommandDetails = RedisCommandDetails("get", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val getexCommandDetails = RedisCommandDetails("getex", -2, arrayListOf("write", "fast"), 1, 1, 1)
    val mgetCommandDetails = RedisCommandDetails("mget", -2, arrayListOf("readonly", "fast"), 1, -1, 1)
    val getsetCommandDetails = RedisCommandDetails("getset", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val getdelCommandDetails = RedisCommandDetails("getdel", 2, arrayListOf("write", "fast"), 1, 1, 1)
    val setCommandDetails = RedisCommandDetails("set", -3, arrayListOf("write", "denyoom"), 1, 1, 1)
    val setexCommandDetails = RedisCommandDetails("setex", 4, arrayListOf("write", "denyoom"), 1, 1, 1)
    val psetexCommandDetails = RedisCommandDetails("psetex", 4, arrayListOf("write", "denyoom"), 1, 1, 1)
    val setnxCommandDetails = RedisCommandDetails("setnx", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val msetCommandDetails = RedisCommandDetails("mset", -3, arrayListOf("write", "denyoom"), 1, -1, 2)
    val msetnxCommandDetails = RedisCommandDetails("msetnx", -3, arrayListOf("write", "denyoom"), 1, -1, 2)
    val existsCommandDetails = RedisCommandDetails("exists", -2, arrayListOf("readonly", "fast"), 1, -1, 1)
    val expireCommandDetails = RedisCommandDetails("expire", 3, arrayListOf("write", "fast"), 1, 1, 1)
    val pexpireCommandDetails = RedisCommandDetails("pexpire", 3, arrayListOf("write", "fast"), 1, 1, 1)
    val expireatCommandDetails = RedisCommandDetails("expireat", 3, arrayListOf("write", "fast"), 1, 1, 1)
    val pexpireatCommandDetails = RedisCommandDetails("pexpireat", 3, arrayListOf("write", "fast"), 1, 1, 1)
    val persistCommandDetails = RedisCommandDetails("persist", 2, arrayListOf("write", "fast"), 1, 1, 1)
    val appendCommandDetails = RedisCommandDetails("append", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val incrCommandDetails = RedisCommandDetails("incr", 2, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val incrbyCommandDetails = RedisCommandDetails("incrby", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val incrbyfloatCommandDetails =
        RedisCommandDetails("incrbyfloat", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val decrCommandDetails = RedisCommandDetails("decr", 2, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val decrbyCommandDetails = RedisCommandDetails("decrby", 3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val strlenCommandDetails = RedisCommandDetails("strlen", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val ttlCommandDetails = RedisCommandDetails("ttl", 2, arrayListOf("readonly", "random", "fast"), 1, 1, 1)
    val pttlCommandDetails = RedisCommandDetails("pttl", 2, arrayListOf("readonly", "random", "fast"), 1, 1, 1)
    val delCommandDetails = RedisCommandDetails("del", -2, arrayListOf("write"), 1, -1, 1)
    val unlinkCommandDetails = RedisCommandDetails("unlink", -2, arrayListOf("write", "fast"), 1, -1, 1)
    val randomkeyCommandDetails = RedisCommandDetails("randomkey", 1, arrayListOf("readonly", "random"), 0, 0, 0)
    val touchCommandDetails = RedisCommandDetails("touch", -2, arrayListOf("readonly", "fast"), 1, -1, 1)
    val typeCommandDetails = RedisCommandDetails("type", 2, arrayListOf("readonly", "fast"), 1, 1, 1)

    val lpushCommandDetails = RedisCommandDetails("lpush", -3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val lpushxCommandDetails = RedisCommandDetails("lpushx", -3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val rpushCommandDetails = RedisCommandDetails("rpush", -3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val rpushxCommandDetails = RedisCommandDetails("rpushx", -3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val lindexCommandDetails = RedisCommandDetails("lindex", 3, arrayListOf("readonly"), 1, 1, 1)
    val llenCommandDetails = RedisCommandDetails("llen", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val lpopCommandDetails = RedisCommandDetails("lpop", -2, arrayListOf("write", "fast"), 1, 1, 1)
    val rpopCommandDetails = RedisCommandDetails("rpop", -2, arrayListOf("write", "fast"), 1, 1, 1)
    val lrangeCommandDetails = RedisCommandDetails("lrange", 4, arrayListOf("readonly"), 1, 1, 1)

    val hsetCommandDetails = RedisCommandDetails("hset", -4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val hsetnxCommandDetails = RedisCommandDetails("hsetnx", 4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val hmsetCommandDetails = RedisCommandDetails("hmset", -4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val saddCommandDetails = RedisCommandDetails("sadd", -3, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val hexistsCommandDetails = RedisCommandDetails("hexists", 3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val sismemberCommandDetails = RedisCommandDetails("sismember", 3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val smismemberCommandDetails = RedisCommandDetails("smismember", -3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val hgetCommandDetails = RedisCommandDetails("hget", 3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val hmgetCommandDetails = RedisCommandDetails("hmget", -3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val hgetallCommandDetails = RedisCommandDetails("hgetall", 2, arrayListOf("readonly", "random"), 1, 1, 1)
    val hvalsCommandDetails = RedisCommandDetails("hvals", 2, arrayListOf("readonly", "sort_for_script"), 1, 1, 1)
    val hkeysCommandDetails = RedisCommandDetails("hkeys", 2, arrayListOf("readonly", "sort_for_script"), 1, 1, 1)
    val smembersCommandDetails = RedisCommandDetails("smembers", 2, arrayListOf("readonly", "sort_for_script"), 1, 1, 1)
    val hincrbyCommandDetails = RedisCommandDetails("hincrby", 4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val hincrbyfloatCommandDetails =
        RedisCommandDetails("hincrbyfloat", 4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val hstrlenCommandDetails = RedisCommandDetails("hstrlen", 3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val hlenCommandDetails = RedisCommandDetails("hlen", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val scardCommandDetails = RedisCommandDetails("scard", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val zcardCommandDetails = RedisCommandDetails("zcard", 2, arrayListOf("readonly", "fast"), 1, 1, 1)
    val hdelCommandDetails = RedisCommandDetails("hdel", -3, arrayListOf("write", "fast"), 1, 1, 1)
    val sremCommandDetails = RedisCommandDetails("srem", -3, arrayListOf("write", "fast"), 1, 1, 1)
    val zremCommandDetails = RedisCommandDetails("zrem", -3, arrayListOf("write", "fast"), 1, 1, 1)
    val sunionCommandDetails = RedisCommandDetails("sunion", -2, arrayListOf("readonly", "sort_for_script"), 1, -1, 1)
    val sinterCommandDetails = RedisCommandDetails("sinter", -2, arrayListOf("readonly", "sort_for_script"), 1, -1, 1)
    val sunionstoreCommandDetails = RedisCommandDetails("sunionstore", -3, arrayListOf("write", "denyoom"), 1, -1, 1)
    val sinterstoreCommandDetails = RedisCommandDetails("sinterstore", -3, arrayListOf("write", "denyoom"), 1, -1, 1)
    val zmscoreCommandDetails = RedisCommandDetails("zmscore", -3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val zrankCommandDetails = RedisCommandDetails("zrank", 3, arrayListOf("readonly", "fast"), 1, 1, 1)
    val zincrbyCommandDetails = RedisCommandDetails("zincrby", 4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val zaddCommandDetails = RedisCommandDetails("zadd", -4, arrayListOf("write", "denyoom", "fast"), 1, 1, 1)
    val zpopmaxCommandDetails = RedisCommandDetails("zpopmax", -2, arrayListOf("write", "fast"), 1, 1, 1)
    val zpopminCommandDetails = RedisCommandDetails("zpopmin", -2, arrayListOf("write", "fast"), 1, 1, 1)
    val zrandmemberCommandDetails = RedisCommandDetails("zrandmember", -2, arrayListOf("readonly", "random"), 1, 1, 1)
    val srandmemberCommandDetails = RedisCommandDetails("srandmember", -2, arrayListOf("readonly", "random"), 1, 1, 1)
    val hrandfieldCommandDetails = RedisCommandDetails("hrandfield", -2, arrayListOf("readonly", "random"), 1, 1, 1)
    val zcountCommandDetails = RedisCommandDetails("zcount", 4, arrayListOf("readonly", "fast"), 1, 1, 1)
    val zlexcountCommandDetails = RedisCommandDetails("zlexcount", 4, arrayListOf("readonly", "fast"), 1, 1, 1)
    val zremrangebyscoreCommandDetails = RedisCommandDetails("zremrangebyscore", 4, arrayListOf("write"), 1, 1, 1)
    val zremrangebyrankCommandDetails = RedisCommandDetails("zremrangebyrank", 4, arrayListOf("write"), 1, 1, 1)
    val zremrangebylexCommandDetails = RedisCommandDetails("zremrangebylex", 4, arrayListOf("write"), 1, 1, 1)
    val zrangeCommandDetails = RedisCommandDetails("zrange", -4, arrayListOf("readonly"), 1, 1, 1)
    val zrangestoreCommandDetails = RedisCommandDetails("zrangestore", -5, arrayListOf("write", "denyoom"), 1, 2, 1)
    val zrevrangeCommandDetails = RedisCommandDetails("zrevrange", -4, arrayListOf("readonly"), 1, 1, 1)
    val zrangebyscoreCommandDetails = RedisCommandDetails("zrangebyscore", -4, arrayListOf("readonly"), 1, 1, 1)
    val zrevrangebyscoreCommandDetails = RedisCommandDetails("zrevrangebyscore", -4, arrayListOf("readonly"), 1, 1, 1)
    val zrangebylexCommandDetails = RedisCommandDetails("zrangebylex", -4, arrayListOf("readonly"), 1, 1, 1)
    val zrevrangebylexCommandDetails = RedisCommandDetails("zrevrangebylex", -4, arrayListOf("readonly"), 1, 1, 1)

    val scanCommandDetails = RedisCommandDetails("scan", -2, arrayListOf("readonly", "random"), 0, 0, 0)
    val hscanCommandDetails = RedisCommandDetails("hscan", -3, arrayListOf("readonly", "random"), 1, 1, 1)
    val sscanCommandDetails = RedisCommandDetails("sscan", -3, arrayListOf("readonly", "random"), 1, 1, 1)
    val zscanCommandDetails = RedisCommandDetails("zscan", -3, arrayListOf("readonly", "random"), 1, 1, 1)
    val keysCommandDetails = RedisCommandDetails("keys", 2, arrayListOf("readonly", "sort_for_script"), 0, 0, 0)

    val flushdbCommandDetails = RedisCommandDetails("flushdb", -1, arrayListOf("write"), 0, 0, 0)
    val flushallCommandDetails = RedisCommandDetails("flushall", -1, arrayListOf("write"), 0, 0, 0)
    val dbsizeCommandDetails = RedisCommandDetails("dbsize", 1, arrayListOf("readonly", "fast"), 0, 0, 0)

    val pingCommandDetails = RedisCommandDetails("ping", -1, arrayListOf("stale", "fast"), 0, 0, 0)
    val echoCommandDetails = RedisCommandDetails("echo", 2, arrayListOf("fast"), 0, 0, 0)
    val lolwutCommandDetails = RedisCommandDetails("lolwut", -1, arrayListOf("readonly", "fast"), 0, 0, 0)
    val timeCommandDetails = RedisCommandDetails("time", 1, arrayListOf("random", "loading", "stale", "fast"), 0, 0, 0)
    val resetCommandDetails =
        RedisCommandDetails("reset", 1, arrayListOf("noscript", "loading", "stale", "fast"), 0, 0, 0)
    val saveCommandDetails = RedisCommandDetails("save", 1, arrayListOf("admin", "noscript"), 0, 0, 0)
    val bgsaveCommandDetails = RedisCommandDetails("bgsave", -1, arrayListOf("admin", "noscript"), 0, 0, 0)
    val commandCommandDetails = RedisCommandDetails("command", -1, arrayListOf("random", "loading", "stale"), 0, 0, 0)

    val authCommandDetails = RedisCommandDetails(
        "auth",
        -2,
        arrayListOf("noscript", "loading", "stale", "skip_monitor", "skip_slowlog", "fast", "no_auth"),
        0,
        0,
        0
    )

    val multiCommandDetails = RedisCommandDetails(
        "multi",
        1,
        arrayListOf("noscript", "loading", "stale", "fast"),
        0,
        0,
        0
    )

    val discardCommandDetails = RedisCommandDetails(
        "discard",
        1,
        arrayListOf("noscript", "loading", "stale", "fast"),
        0,
        0,
        0
    )

    val execCommandDetails = RedisCommandDetails(
        "exec",
        1,
        arrayListOf("noscript", "loading", "stale", "skip_monitor", "skip_slowlog"),
        0,
        0,
        0
    )
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/listener/map/HexistsCommandListener.kt<|end_filename|>
package com.aerospike.skyhook.listener.map

import com.aerospike.client.Key
import com.aerospike.client.Record
import com.aerospike.client.Value
import com.aerospike.client.cdt.MapOperation
import com.aerospike.client.cdt.MapReturnType
import com.aerospike.client.listener.RecordListener
import com.aerospike.skyhook.command.RequestCommand
import com.aerospike.skyhook.listener.BaseListener
import com.aerospike.skyhook.util.Typed
import io.netty.channel.ChannelHandlerContext

class HexistsCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx), RecordListener {

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount == 3) { argValidationErrorMsg(cmd) }

        val key = createKey(cmd.key)
        val operation = MapOperation.getByKey(
            aeroCtx.bin, Typed.getValue(cmd.args[2]),
            MapReturnType.COUNT
        )
        client.operate(
            null, this, defaultWritePolicy,
            key, operation
        )
    }

    override fun onSuccess(key: Key?, record: Record?) {
        if (record == null) {
            writeLong(0L)
        } else {
            writeResponse(record.bins[aeroCtx.bin])
        }
        flushCtxTransactionAware()
    }
}

class SmismemberCommandListener(
    ctx: ChannelHandlerContext
) : BaseListener(ctx) {

    override fun handle(cmd: RequestCommand) {
        require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) }

        val key = createKey(cmd.key)
        val values = getValues(cmd)
        writeArrayHeader(values.size.toLong())

        values.forEach { v ->
            val operation = MapOperation.getByKey(
                aeroCtx.bin, v, MapReturnType.COUNT
            )
            val exists = client.operate(
                defaultWritePolicy,
                key, operation
            )?.getLong(aeroCtx.bin) ?: 0L
            writeLong(exists)
        }
        flushCtxTransactionAware()
    }

    private fun getValues(cmd: RequestCommand): List<Value> {
        return cmd.args.drop(2)
            .map { Typed.getValue(it) }
    }
}


<|start_filename|>src/main/kotlin/com/aerospike/skyhook/config/ClientPolicyConfig.kt<|end_filename|>
package com.aerospike.skyhook.config

import com.aerospike.client.policy.AuthMode

/**
 * Aerospike Java Client [com.aerospike.client.policy.ClientPolicy] configuration properties.
 */
data class ClientPolicyConfig(

    /**
     * User authentication to cluster.
     * Leave null for clusters running without restricted access.
     */
    val user: String? = null,

    /**
     * Password authentication to cluster.
     * The password will be stored by the client and sent to server in hashed format.
     * Leave null for clusters running without restricted access.
     */
    val password: String? = null,

    /**
     * Expected cluster name. If not null, server nodes must return this cluster name in order to join
     * the client's view of the cluster. Should only be set when connecting to servers that support the
     * "cluster-name" info command.
     */
    val clusterName: String? = null,

    /**
     * Authentication mode used when user/password is defined.
     */
    val authMode: AuthMode? = null,

    /**
     * Initial host connection timeout in milliseconds.
     * The timeout when opening a connection to the server host for the first time.
     */
    val timeout: Int? = null,

    /**
     * Login timeout in milliseconds. The timeout is used when user authentication is enabled
     * and a node login is being performed.
     */
    val loginTimeout: Int? = null,

    /**
     * Minimum number of asynchronous connections allowed per server node.
     * Preallocate min connections on client node creation. The client will periodically allocate new connections
     * if count falls below min connections.
     */
    val asyncMinConnsPerNode: Int? = null,

    /**
     * Maximum number of asynchronous connections allowed per server node.
     * Transactions will go through retry logic and potentially fail with "ResultCode.NO_MORE_CONNECTIONS"
     * if the maximum number of connections would be exceeded.
     */
    val asyncMaxConnsPerNode: Int? = null,

    /**
     * Throw exception if all seed connections fail on cluster instantiation.
     */
    val failIfNotConnected: Boolean? = null,

    /**
     * Should use "services-alternate" instead of "services" in info request during cluster tending.
     * "services-alternate" returns server configured external IP addresses that client uses to talk to nodes.
     */
    val useServicesAlternate: Boolean? = null,
)
