values() {
}
private static final class ByteArrayWrapper implements Serializable {
+ private static final long serialVersionUID = 1L;
private final byte[] data;
+ private final int hashCode;
public ByteArrayWrapper(byte[] data) {
if (data == null) {
throw new NullPointerException();
}
this.data = data;
+ this.hashCode = Arrays.hashCode(data);
}
@Override
@@ -109,7 +112,7 @@ public boolean equals(Object other) {
@Override
public int hashCode() {
- return Arrays.hashCode(data);
+ return hashCode;
}
}
diff --git a/src/main/java/redis/clients/jedis/util/SafeEncoder.java b/src/main/java/redis/clients/jedis/util/SafeEncoder.java
index fb5d8fb24e..78338ea2e5 100644
--- a/src/main/java/redis/clients/jedis/util/SafeEncoder.java
+++ b/src/main/java/redis/clients/jedis/util/SafeEncoder.java
@@ -63,4 +63,32 @@ public static Object encodeObject(Object dataToEncode) {
return dataToEncode;
}
+
+ /**
+ * Converts a byte array to uppercase by converting lowercase ASCII letters (a-z) to uppercase (A-Z).
+ * This method is optimized for ASCII text and performs direct byte manipulation, which is significantly
+ * faster than converting to String and calling String.toUpperCase().
+ *
+ * This method only works correctly for ASCII text. Non-ASCII characters are left unchanged.
+ * For Redis command names (which are always ASCII), this is safe and provides ~47% performance
+ * improvement over the String-based approach.
+ *
+ * @param data the byte array to convert to uppercase
+ * @return a new byte array with lowercase ASCII letters converted to uppercase
+ */
+ public static byte[] toUpperCase(final byte[] data) {
+ if (data == null) {
+ return null;
+ }
+
+ byte[] uppercaseBytes = new byte[data.length];
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] >= 'a' && data[i] <= 'z') {
+ uppercaseBytes[i] = (byte) (data[i] - 32);
+ } else {
+ uppercaseBytes[i] = data[i];
+ }
+ }
+ return uppercaseBytes;
+ }
}
diff --git a/src/test/java/redis/clients/jedis/ClusterCommandExecutorTest.java b/src/test/java/redis/clients/jedis/ClusterCommandExecutorTest.java
index 0ff04a832a..955dfff642 100644
--- a/src/test/java/redis/clients/jedis/ClusterCommandExecutorTest.java
+++ b/src/test/java/redis/clients/jedis/ClusterCommandExecutorTest.java
@@ -37,7 +37,8 @@ public class ClusterCommandExecutorTest {
@Test
public void runSuccessfulExecute() {
ClusterConnectionProvider connectionHandler = mock(ClusterConnectionProvider.class);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, Duration.ZERO) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, Duration.ZERO,
+ StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
return (T) "foo";
@@ -53,7 +54,8 @@ protected void sleep(long ignored) {
@Test
public void runFailOnFirstExecSuccessOnSecondExec() {
ClusterConnectionProvider connectionHandler = mock(ClusterConnectionProvider.class);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
boolean isFirstCall = true;
@Override
@@ -79,7 +81,8 @@ protected void sleep(long ignored) {
public void runAlwaysFailing() {
ClusterConnectionProvider connectionHandler = mock(ClusterConnectionProvider.class);
final LongConsumer sleep = mock(LongConsumer.class);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 3, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 3, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
throw new JedisConnectionException("Connection failed");
@@ -109,7 +112,8 @@ protected void sleep(long sleepMillis) {
public void runMovedSuccess() {
ClusterConnectionProvider connectionHandler = mock(ClusterConnectionProvider.class);
final HostAndPort movedTarget = new HostAndPort(null, 0);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
boolean isFirstCall = true;
@Override
@@ -146,7 +150,8 @@ public void runAskSuccess() {
final HostAndPort askTarget = new HostAndPort(null, 0);
when(connectionHandler.getConnection(askTarget)).thenReturn(connection);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
boolean isFirstCall = true;
@Override
@@ -199,7 +204,8 @@ public void runMovedThenAllNodesFailing() {
final LongConsumer sleep = mock(LongConsumer.class);
final HostAndPort movedTarget = new HostAndPort(null, 0);
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 5, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 5, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
if (redirecter == connection) {
@@ -266,7 +272,8 @@ public void runMasterFailingReplicaRecovering() {
}).when(connectionHandler).renewSlotCache();
final AtomicLong totalSleepMs = new AtomicLong();
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 5, ONE_SECOND) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 5, ONE_SECOND,
+ StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
@@ -304,7 +311,7 @@ public void runRethrowsJedisNoReachableClusterNodeException() {
JedisClusterOperationException.class);
ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 10,
- Duration.ZERO) {
+ Duration.ZERO, StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
return null;
@@ -325,7 +332,8 @@ public void runStopsRetryingAfterTimeout() {
//final LongConsumer sleep = mock(LongConsumer.class);
final AtomicLong totalSleepMs = new AtomicLong();
- ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 3, Duration.ZERO) {
+ ClusterCommandExecutor testMe = new ClusterCommandExecutor(connectionHandler, 3, Duration.ZERO,
+ StaticCommandFlagsRegistry.registry()) {
@Override
public T execute(Connection connection, CommandObject commandObject) {
try {
diff --git a/src/test/java/redis/clients/jedis/StaticCommandFlagsRegistryTest.java b/src/test/java/redis/clients/jedis/StaticCommandFlagsRegistryTest.java
new file mode 100644
index 0000000000..122ed6272b
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/StaticCommandFlagsRegistryTest.java
@@ -0,0 +1,176 @@
+package redis.clients.jedis;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.EnumSet;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import redis.clients.jedis.CommandFlagsRegistry.CommandFlag;
+import redis.clients.jedis.commands.ProtocolCommand;
+import redis.clients.jedis.util.SafeEncoder;
+
+/**
+ * Unit tests for StaticCommandFlagsRegistry. Tests the retrieval of command flags for various Redis
+ * commands, including commands with subcommands.
+ */
+public class StaticCommandFlagsRegistryTest {
+
+ private StaticCommandFlagsRegistry registry;
+
+ @BeforeEach
+ public void setUp() {
+ registry = StaticCommandFlagsRegistry.registry();
+ }
+
+ /**
+ * Test that FUNCTION LOAD command returns the correct flags. FUNCTION LOAD should have: DENYOOM,
+ * NOSCRIPT, WRITE flags.
+ */
+ @Test
+ public void testFunctionLoadCommandFlags() {
+ // Create a CommandArguments for "FUNCTION LOAD"
+ CommandArguments functionLoadArgs = new CommandArguments(Protocol.Command.FUNCTION).add("LOAD");
+
+ EnumSet flags = registry.getFlags(functionLoadArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "FUNCTION LOAD should have flags");
+ assertEquals(3, flags.size(), "FUNCTION LOAD should have exactly 3 flags");
+ assertTrue(flags.contains(CommandFlag.DENYOOM), "FUNCTION LOAD should have DENYOOM flag");
+ assertTrue(flags.contains(CommandFlag.NOSCRIPT), "FUNCTION LOAD should have NOSCRIPT flag");
+ assertTrue(flags.contains(CommandFlag.WRITE), "FUNCTION LOAD should have WRITE flag");
+ }
+
+ /**
+ * Test that FUNCTION DELETE command returns the correct flags. FUNCTION DELETE should have:
+ * NOSCRIPT, WRITE flags.
+ */
+ @Test
+ public void testFunctionDeleteCommandFlags() {
+ CommandArguments functionDeleteArgs = new CommandArguments(Protocol.Command.FUNCTION)
+ .add("DELETE");
+
+ EnumSet flags = registry.getFlags(functionDeleteArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "FUNCTION DELETE should have flags");
+ assertEquals(2, flags.size(), "FUNCTION DELETE should have exactly 2 flags");
+ assertTrue(flags.contains(CommandFlag.NOSCRIPT), "FUNCTION DELETE should have NOSCRIPT flag");
+ assertTrue(flags.contains(CommandFlag.WRITE), "FUNCTION DELETE should have WRITE flag");
+ }
+
+ /**
+ * Test other subcommand examples: ACL SETUSER
+ */
+ @Test
+ public void testAclSetUserCommandFlags() {
+ CommandArguments aclSetUserArgs = new CommandArguments(Protocol.Command.ACL).add("SETUSER");
+
+ EnumSet flags = registry.getFlags(aclSetUserArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "ACL SETUSER should have flags");
+ assertTrue(flags.contains(CommandFlag.ADMIN), "ACL SETUSER should have ADMIN flag");
+ assertTrue(flags.contains(CommandFlag.NOSCRIPT), "ACL SETUSER should have NOSCRIPT flag");
+ }
+
+ /**
+ * Test other subcommand examples: CONFIG GET
+ */
+ @Test
+ public void testConfigGetCommandFlags() {
+ CommandArguments configGetArgs = new CommandArguments(Protocol.Command.CONFIG).add("GET");
+
+ EnumSet flags = registry.getFlags(configGetArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "CONFIG GET should have flags");
+ assertTrue(flags.contains(CommandFlag.ADMIN), "CONFIG GET should have ADMIN flag");
+ assertTrue(flags.contains(CommandFlag.LOADING), "CONFIG GET should have LOADING flag");
+ assertTrue(flags.contains(CommandFlag.STALE), "CONFIG GET should have STALE flag");
+ }
+
+ /**
+ * Test simple command without subcommands: GET
+ */
+ @Test
+ public void testGetCommandFlags() {
+ CommandArguments getArgs = new CommandArguments(Protocol.Command.GET).add("key");
+
+ EnumSet flags = registry.getFlags(getArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "GET should have flags");
+ assertTrue(flags.contains(CommandFlag.READONLY), "GET should have READONLY flag");
+ assertTrue(flags.contains(CommandFlag.FAST), "GET should have FAST flag");
+ }
+
+ /**
+ * Test simple command without subcommands: SET
+ */
+ @Test
+ public void testSetCommandFlags() {
+ CommandArguments setArgs = new CommandArguments(Protocol.Command.SET).add("key").add("value");
+
+ EnumSet flags = registry.getFlags(setArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "SET should have flags");
+ assertTrue(flags.contains(CommandFlag.WRITE), "SET should have WRITE flag");
+ assertTrue(flags.contains(CommandFlag.DENYOOM), "SET should have DENYOOM flag");
+ }
+
+ /**
+ * Test that unknown commands return empty flags
+ */
+ @Test
+ public void testUnknownCommandReturnsEmptyFlags() {
+ ProtocolCommand unknownCommand = () -> SafeEncoder.encode("UNKNOWN_COMMAND_XYZ");
+ CommandArguments unknownArgs = new CommandArguments(unknownCommand);
+
+ EnumSet flags = registry.getFlags(unknownArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertTrue(flags.isEmpty(), "Unknown command should return empty flags");
+ }
+
+ /**
+ * Test case insensitivity - command names should be normalized to uppercase
+ */
+ @Test
+ public void testCaseInsensitivity() {
+ ProtocolCommand functionCommand = () -> SafeEncoder.encode("function");
+ CommandArguments functionLoadArgs = new CommandArguments(functionCommand).add("load");
+
+ EnumSet flags = registry.getFlags(functionLoadArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertFalse(flags.isEmpty(), "function load (lowercase) should have flags");
+ assertEquals(3, flags.size(), "function load should have exactly 3 flags");
+ assertTrue(flags.contains(CommandFlag.DENYOOM), "function load should have DENYOOM flag");
+ assertTrue(flags.contains(CommandFlag.NOSCRIPT), "function load should have NOSCRIPT flag");
+ assertTrue(flags.contains(CommandFlag.WRITE), "function load should have WRITE flag");
+ }
+
+ /**
+ * Test that unknown subcommands of parent commands fall back to parent command flags. If the
+ * parent command also doesn't exist, it should return empty flags.
+ */
+ @Test
+ public void testUnknownSubcommandFallback() {
+ // Create a CommandArguments for "FUNCTION UNKNOWN_SUBCOMMAND"
+ // This subcommand doesn't exist, so it should fall back to "FUNCTION" parent flags
+ // Since "FUNCTION" parent has empty flags, it should return empty flags
+ CommandArguments unknownSubcommandArgs = new CommandArguments(Protocol.Command.FUNCTION)
+ .add("UNKNOWN_SUBCOMMAND");
+
+ EnumSet flags = registry.getFlags(unknownSubcommandArgs);
+
+ assertNotNull(flags, "Flags should not be null");
+ assertTrue(flags.isEmpty(),
+ "Unknown FUNCTION subcommand should return empty flags (parent flags)");
+ }
+}
diff --git a/src/test/java/redis/clients/jedis/codegen/CommandFlagsRegistryGenerator.java b/src/test/java/redis/clients/jedis/codegen/CommandFlagsRegistryGenerator.java
new file mode 100644
index 0000000000..009ef07d05
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/codegen/CommandFlagsRegistryGenerator.java
@@ -0,0 +1,513 @@
+package redis.clients.jedis.codegen;
+
+import com.google.gson.Gson;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Module;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Code generator for StaticCommandFlagsRegistry. This generator connects to a Redis server,
+ * retrieves all command metadata using the COMMAND command, and automatically generates the
+ * StaticCommandFlagsRegistry class that implements CommandFlagsRegistry interface.
+ *
+ * Usage:
+ *
+ *
+ * java -cp ... redis.clients.jedis.codegen.CommandFlagsRegistryGenerator [host] [port]
+ *
+ *
+ * Arguments:
+ *
+ * - host - Redis server hostname (default: localhost)
+ * - port - Redis server port (default: 6379)
+ *
+ *
+ * Note: This is a code generation tool and should NOT be executed as part of regular tests.
+ */
+public class CommandFlagsRegistryGenerator {
+
+ private static final String JAVA_FILE = "src/main/java/redis/clients/jedis/StaticCommandFlagsRegistryInitializer.java";
+ private static final String BACKUP_JSON_FILE = "redis_commands_flags.json";
+
+ private final String redisHost;
+ private final int redisPort;
+
+ // Server metadata collected during generation
+ private ServerMetadata serverMetadata;
+
+ // Map JSON flag names to Java enum names
+ private static final Map FLAG_MAPPING = new LinkedHashMap<>();
+ static {
+ FLAG_MAPPING.put("readonly", "READONLY");
+ FLAG_MAPPING.put("write", "WRITE");
+ FLAG_MAPPING.put("denyoom", "DENYOOM");
+ FLAG_MAPPING.put("admin", "ADMIN");
+ FLAG_MAPPING.put("pubsub", "PUBSUB");
+ FLAG_MAPPING.put("noscript", "NOSCRIPT");
+ FLAG_MAPPING.put("random", "RANDOM");
+ FLAG_MAPPING.put("sort_for_script", "SORT_FOR_SCRIPT");
+ FLAG_MAPPING.put("loading", "LOADING");
+ FLAG_MAPPING.put("stale", "STALE");
+ FLAG_MAPPING.put("skip_monitor", "SKIP_MONITOR");
+ FLAG_MAPPING.put("skip_slowlog", "SKIP_SLOWLOG");
+ FLAG_MAPPING.put("asking", "ASKING");
+ FLAG_MAPPING.put("fast", "FAST");
+ FLAG_MAPPING.put("movablekeys", "MOVABLEKEYS");
+ FLAG_MAPPING.put("module", "MODULE");
+ FLAG_MAPPING.put("blocking", "BLOCKING");
+ FLAG_MAPPING.put("no_auth", "NO_AUTH");
+ FLAG_MAPPING.put("no_async_loading", "NO_ASYNC_LOADING");
+ FLAG_MAPPING.put("no_multi", "NO_MULTI");
+ FLAG_MAPPING.put("no_mandatory_keys", "NO_MANDATORY_KEYS");
+ FLAG_MAPPING.put("allow_busy", "ALLOW_BUSY");
+ }
+
+ public CommandFlagsRegistryGenerator(String host, int port) {
+ this.redisHost = host;
+ this.redisPort = port;
+ }
+
+ public static void main(String[] args) {
+ printLine();
+ System.out.println("StaticCommandFlagsRegistry Generator");
+ printLine();
+
+ // Parse command line arguments
+ String host = args.length > 0 ? args[0] : "localhost";
+ int port = args.length > 1 ? Integer.parseInt(args[1]) : 6379;
+
+ System.out.println("Redis server: " + host + ":" + port);
+ System.out.println();
+
+ try {
+ CommandFlagsRegistryGenerator generator = new CommandFlagsRegistryGenerator(host, port);
+ generator.generate();
+
+ System.out.println();
+ printLine();
+ System.out.println("✓ Code generation completed successfully!");
+ printLine();
+ } catch (Exception e) {
+ System.err.println();
+ printLine();
+ System.err.println("✗ Code generation failed!");
+ printLine();
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void printLine() {
+ for (int i = 0; i < 80; i++) {
+ System.out.print("=");
+ }
+ System.out.println();
+ }
+
+ public void generate() throws IOException {
+ Map> commandsFlags;
+
+ // Step 1: Retrieve commands from Redis
+ System.out.println("\nStep 1: Connecting to Redis at " + redisHost + ":" + redisPort + "...");
+ try {
+ commandsFlags = retrieveCommandsFromRedis();
+ System.out.println("✓ Retrieved " + commandsFlags.size() + " commands from Redis");
+
+ // Save to backup JSON file
+ saveToJsonFile(commandsFlags);
+ } catch (JedisConnectionException e) {
+ System.err.println("✗ Failed to connect to Redis: " + e.getMessage());
+ System.out.println("\nAttempting to use backup JSON file: " + BACKUP_JSON_FILE);
+ commandsFlags = readJsonFile();
+ System.out.println("✓ Loaded " + commandsFlags.size() + " commands from backup file");
+ }
+
+ // Step 2: Process commands and group by flag combinations
+ System.out.println("\nStep 2: Processing commands and grouping by flags...");
+ Map> flagCombinations = groupByFlags(commandsFlags);
+ System.out.println("✓ Found " + flagCombinations.size() + " unique flag combinations");
+
+ // Step 3: Generate StaticCommandFlagsRegistry class
+ System.out.println("\nStep 3: Generating StaticCommandFlagsRegistry class...");
+ String classContent = generateRegistryClass(flagCombinations);
+ System.out.println("✓ Generated " + classContent.split("\n").length + " lines of code");
+
+ // Step 4: Write StaticCommandFlagsRegistry.java
+ System.out.println("\nStep 4: Writing " + JAVA_FILE + "...");
+ writeJavaFile(classContent);
+ System.out.println("✓ Successfully created StaticCommandFlagsRegistry.java");
+ }
+
+ private Map> retrieveCommandsFromRedis() {
+ Map> result = new LinkedHashMap<>();
+
+ try (Jedis jedis = new Jedis(redisHost, redisPort)) {
+ jedis.connect();
+
+ // Collect server metadata
+ String infoServer = jedis.info("server");
+ String version = extractInfoValue(infoServer, "redis_version");
+ String mode = extractInfoValue(infoServer, "redis_mode");
+
+ // Get loaded modules
+ List modules = new ArrayList<>();
+ try {
+ List moduleList = jedis.moduleList();
+ for (Module module : moduleList) {
+ modules.add(module.getName());
+ }
+ } catch (Exception e) {
+ // Module list might not be available in all Redis versions
+ System.out.println(" Note: Could not retrieve module list: " + e.getMessage());
+ }
+
+ serverMetadata = new ServerMetadata(version, mode, modules);
+
+ // Get all commands using COMMAND
+ Map commands = jedis.command();
+
+ for (Map.Entry entry : commands.entrySet()) {
+ redis.clients.jedis.resps.CommandInfo cmdInfo = entry.getValue();
+ String commandName = normalizeCommandName(cmdInfo.getName());
+
+ // Get flags
+ List flags = new ArrayList<>();
+ if (cmdInfo.getFlags() != null) {
+ for (String flag : cmdInfo.getFlags()) {
+ flags.add(flag.toLowerCase());
+ }
+ }
+
+ // Check for subcommands
+ Map subcommands = cmdInfo.getSubcommands();
+
+ if (subcommands != null && !subcommands.isEmpty()) {
+ // This command has subcommands - process them instead of the parent
+ for (Map.Entry subEntry : subcommands
+ .entrySet()) {
+ redis.clients.jedis.resps.CommandInfo subCmdInfo = subEntry.getValue();
+ String subCommandName = normalizeCommandName(subCmdInfo.getName());
+
+ // Filter out unwanted commands
+ if (shouldExcludeCommand(subCommandName)) {
+ continue;
+ }
+
+ // Get subcommand flags
+ List subFlags = new ArrayList<>();
+ if (subCmdInfo.getFlags() != null) {
+ for (String flag : subCmdInfo.getFlags()) {
+ subFlags.add(flag.toLowerCase());
+ }
+ }
+
+ result.put(subCommandName, subFlags);
+ }
+ } else {
+ // Regular command without subcommands
+ // Filter out unwanted commands
+ if (!shouldExcludeCommand(commandName)) {
+ result.put(commandName, flags);
+ }
+ }
+ }
+
+ }
+ // Ignore close errors
+
+ return result;
+ }
+
+ /**
+ * Normalize command name: replace pipe separators with spaces and convert to uppercase. Redis
+ * returns command names like "acl|help" but Jedis uses "ACL HELP".
+ */
+ private String normalizeCommandName(String commandName) {
+ return commandName.replace('|', ' ').toUpperCase();
+ }
+
+ /**
+ * Check if a command should be excluded from the registry.
+ *
+ * Exclusion rules:
+ *
+ * - All HELP subcommands (e.g., "ACL HELP", "CONFIG HELP", "XINFO HELP")
+ * - All FT.DEBUG subcommands (e.g., "FT.DEBUG DUMP_TERMS", "FT.DEBUG GIT_SHA")
+ * - All _FT.DEBUG subcommands (internal RediSearch debug commands)
+ *
+ */
+ private boolean shouldExcludeCommand(String commandName) {
+ // Exclude all HELP subcommands
+ if (commandName.endsWith(" HELP")) {
+ return true;
+ }
+
+ // Exclude FT.DEBUG and _FT.DEBUG subcommands
+ return commandName.startsWith("FT.DEBUG ") || commandName.startsWith("_FT.DEBUG ");
+ }
+
+ private String extractInfoValue(String info, String key) {
+ String[] lines = info.split("\n");
+ for (String line : lines) {
+ if (line.startsWith(key + ":")) {
+ return line.substring(key.length() + 1).trim();
+ }
+ }
+ return "unknown";
+ }
+
+ private void saveToJsonFile(Map> commandsFlags) throws IOException {
+ Gson gson = new Gson();
+ String json = gson.toJson(commandsFlags);
+
+ Path jsonPath = Paths.get(BACKUP_JSON_FILE);
+ Files.write(jsonPath, json.getBytes(StandardCharsets.UTF_8));
+ System.out.println("✓ Saved backup to " + BACKUP_JSON_FILE);
+ }
+
+ private Map> readJsonFile() throws IOException {
+ Path jsonPath = Paths.get(BACKUP_JSON_FILE);
+ if (!Files.exists(jsonPath)) {
+ throw new IOException("Backup file not found: " + BACKUP_JSON_FILE);
+ }
+
+ // JDK 8 compatible: read file as bytes and convert to string
+ byte[] bytes = Files.readAllBytes(jsonPath);
+ String jsonContent = new String(bytes, StandardCharsets.UTF_8);
+
+ Gson gson = new Gson();
+
+ // Parse JSON manually to preserve order
+ @SuppressWarnings("unchecked")
+ Map> parsed = gson.fromJson(jsonContent, Map.class);
+
+ return new LinkedHashMap<>(parsed);
+ }
+
+ private Map> groupByFlags(Map> commandsFlags) {
+ Map> result = new LinkedHashMap<>();
+
+ for (Map.Entry> entry : commandsFlags.entrySet()) {
+ String command = entry.getKey();
+ List jsonFlags = entry.getValue();
+
+ // Convert JSON flags to Java enum names and sort
+ List javaFlags = jsonFlags.stream().map(f -> FLAG_MAPPING.get(f.toLowerCase()))
+ .filter(Objects::nonNull).sorted().collect(Collectors.toList());
+
+ FlagSet flagSet = new FlagSet(javaFlags);
+ result.computeIfAbsent(flagSet, k -> new ArrayList<>()).add(command.toUpperCase());
+ }
+
+ return result;
+ }
+
+ private String generateRegistryClass(Map> flagCombinations) {
+ StringBuilder sb = new StringBuilder();
+
+ // Package and imports
+ sb.append("package redis.clients.jedis;\n\n");
+ sb.append("import java.util.EnumSet;\n");
+ sb.append("import static redis.clients.jedis.StaticCommandFlagsRegistry.EMPTY_FLAGS;\n");
+ sb.append("import static redis.clients.jedis.CommandFlagsRegistry.CommandFlag;\n");
+
+ // Class javadoc
+ sb.append("/**\n");
+ sb.append(
+ " * Static implementation of CommandFlagsRegistry. This class is auto-generated by\n");
+ sb.append(" * CommandFlagsRegistryGenerator. DO NOT EDIT MANUALLY.\n");
+
+ // Add server metadata if available
+ if (serverMetadata != null) {
+ sb.append(" * Generated from Redis Server:\n");
+ sb.append(" *
\n");
+ sb.append(" * - Version: ").append(serverMetadata.version).append("
\n");
+ sb.append(" * - Mode: ").append(serverMetadata.mode).append("
\n");
+ if (!serverMetadata.modules.isEmpty()) {
+ sb.append(" * - Loaded Modules: ").append(String.join(", ", serverMetadata.modules))
+ .append("
\n");
+ } else {
+ sb.append(" * - Loaded Modules: none
\n");
+ }
+ sb.append(" * - Generated at: ").append(serverMetadata.generatedAt).append("
\n");
+ sb.append(" *
\n");
+ }
+
+ sb.append(" */\n");
+ sb.append("final class StaticCommandFlagsRegistryInitializer {\n\n");
+
+ // Static initializer block
+ sb.append(" static void initialize(StaticCommandFlagsRegistry.Builder builder) {\n");
+
+ // Organize commands into parent commands and simple commands
+ Map> parentCommands = new LinkedHashMap<>();
+ Map simpleCommands = new LinkedHashMap<>();
+
+ // Known parent commands
+ Set knownParents = new HashSet<>(
+ Arrays.asList("ACL", "CLIENT", "CLUSTER", "COMMAND", "CONFIG", "FUNCTION", "LATENCY",
+ "MEMORY", "MODULE", "OBJECT", "PUBSUB", "SCRIPT", "SLOWLOG", "XGROUP", "XINFO"));
+
+ // Categorize commands
+ for (Map.Entry> entry : flagCombinations.entrySet()) {
+ FlagSet flagSet = entry.getKey();
+ for (String command : entry.getValue()) {
+ int spaceIndex = command.indexOf(' ');
+ if (spaceIndex > 0) {
+ // This is a compound command (e.g., "FUNCTION LOAD")
+ String parent = command.substring(0, spaceIndex);
+ String subcommand = command.substring(spaceIndex + 1);
+
+ if (knownParents.contains(parent)) {
+ parentCommands.computeIfAbsent(parent, k -> new LinkedHashMap<>()).put(subcommand,
+ flagSet);
+ } else {
+ // Not a known parent, treat as simple command
+ simpleCommands.put(command, flagSet);
+ }
+ } else {
+ // Simple command without subcommands
+ simpleCommands.put(command, flagSet);
+ }
+ }
+ }
+
+ // Generate parent command registries
+ for (String parent : knownParents) {
+ sb.append(String.format("builder.register(\"%s\", EMPTY_FLAGS);", parent));
+
+ Map subcommands = parentCommands.get(parent);
+ if (subcommands != null && !subcommands.isEmpty()) {
+ sb.append(String.format(" // %s parent command with subcommands\n", parent));
+ // Add subcommands
+ List sortedSubcommands = new ArrayList<>(subcommands.keySet());
+ Collections.sort(sortedSubcommands);
+
+ for (String subcommand : sortedSubcommands) {
+ FlagSet flagSet = subcommands.get(subcommand);
+ String enumSetExpr = createEnumSetExpression(flagSet.flags);
+ sb.append(String.format("builder.register(\"%s\", \"%s\", %s);\n", parent, subcommand,
+ enumSetExpr));
+ }
+ }
+ }
+
+ // Generate simple commands grouped by flags
+ Map> simpleCommandsByFlags = new LinkedHashMap<>();
+ for (Map.Entry entry : simpleCommands.entrySet()) {
+ simpleCommandsByFlags.computeIfAbsent(entry.getValue(), k -> new ArrayList<>())
+ .add(entry.getKey());
+ }
+
+ // Sort by flag count, then alphabetically
+ List>> sortedEntries = simpleCommandsByFlags.entrySet().stream()
+ .sorted(
+ Comparator.comparing((Map.Entry> e) -> e.getKey().flags.size())
+ .thenComparing(e -> e.getKey().toString()))
+ .collect(Collectors.toList());
+
+ for (Map.Entry> entry : sortedEntries) {
+ FlagSet flagSet = entry.getKey();
+ List commands = entry.getValue();
+ Collections.sort(commands);
+
+ // Add comment
+ String flagDesc = flagSet.flags.isEmpty() ? "no flags"
+ : flagSet.flags.stream().map(String::toLowerCase).collect(Collectors.joining(", "));
+ sb.append(String.format(" // %d command(s) with: %s\n", commands.size(), flagDesc));
+
+ // Generate EnumSet expression
+ String enumSetExpr = createEnumSetExpression(flagSet.flags);
+
+ // Add registry entries using SafeEncoder.encode()
+ for (String command : commands) {
+ sb.append(String.format("builder.register(\"%s\", %s);\n", command, enumSetExpr));
+ }
+ sb.append("\n");
+ }
+
+ // Close initializer block
+ sb.append(" }\n\n");
+
+ // Close class
+ sb.append("}\n");
+
+ return sb.toString();
+ }
+
+ private String createEnumSetExpression(List flags) {
+ if (flags.isEmpty()) {
+ return "EMPTY_FLAGS";
+ } else if (flags.size() == 1) {
+ return "EnumSet.of(CommandFlag." + flags.get(0) + ")";
+ } else {
+ String flagsList = flags.stream().map(f -> "CommandFlag." + f)
+ .collect(Collectors.joining(", "));
+ return "EnumSet.of(" + flagsList + ")";
+ }
+ }
+
+ private void writeJavaFile(String classContent) throws IOException {
+ Path javaPath = Paths.get(JAVA_FILE);
+
+ // JDK 8 compatible: write string as bytes
+ Files.write(javaPath, classContent.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Represents a set of flags for grouping commands
+ */
+ private static class FlagSet {
+ final List flags;
+ final int hashCode;
+
+ FlagSet(List flags) {
+ this.flags = new ArrayList<>(flags);
+ this.hashCode = this.flags.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FlagSet)) return false;
+ FlagSet flagSet = (FlagSet) o;
+ return flags.equals(flagSet.flags);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return flags.toString();
+ }
+ }
+
+ /**
+ * Holds metadata about the Redis server used for generation
+ */
+ private static class ServerMetadata {
+ final String version;
+ final String mode;
+ final List modules;
+ final String generatedAt;
+
+ ServerMetadata(String version, String mode, List modules) {
+ this.version = version;
+ this.mode = mode;
+ this.modules = modules;
+ this.generatedAt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss z")
+ .format(new java.util.Date());
+ }
+ }
+}
diff --git a/src/test/java/redis/clients/jedis/codegen/README.md b/src/test/java/redis/clients/jedis/codegen/README.md
new file mode 100644
index 0000000000..472e7103a4
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/codegen/README.md
@@ -0,0 +1,50 @@
+# Code Generators
+
+This package contains code generation tools for the Jedis project. These are **not tests** and should not be executed as part of the test suite.
+
+## CommandFlagsRegistryGenerator
+
+Automatically generates and updates the static flags registry in `CommandObject.java` by retrieving command metadata from a running Redis server.
+
+### Purpose
+
+The `CommandObject` class uses a static registry to map Redis command names to their flags.
+
+### How It Works
+
+1. **Connects** to a Redis server (default: localhost:6379)
+2. **Retrieves** all command metadata using the `COMMAND` command
+3. **Processes** commands and subcommands, extracting their flags
+4. **Groups** commands by their flag combinations
+5. **Generates** a static initializer block with inline `EnumSet` creation
+6. **Updates** `CommandObject.java` automatically using regex pattern matching
+7. **Saves** a backup JSON file for offline use
+
+### Prerequisites
+
+- A running Redis server (version 7.0+ recommended for full command metadata)
+- The Redis server should have all modules loaded if you want to include module commands
+
+### When to Run
+
+Run this generator whenever:
+- Upgrading to a new Redis version
+- New Redis modules are added to your server
+- Command flags are modified in Redis
+- You want to ensure the registry is up-to-date with your Redis server
+
+### Fallback Mode
+
+If the generator cannot connect to Redis, it will automatically fall back to using the backup JSON file (`redis_commands_flags.json`) if available.
+
+### Output
+
+The generator will:
+- ✓ Connect to Redis and retrieve command metadata
+- ✓ Process commands and subcommands
+- ✓ Group commands by flag combinations
+- ✓ Generate the complete static initializer block
+- ✓ Update `src/main/java/redis/clients/jedis/CommandObject.java` in-place
+- ✓ Save a backup JSON file for offline use
+- ✓ Preserve original command names (with spaces, dots, hyphens)
+