DriverInfo.java
package redis.clients.jedis;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import redis.clients.jedis.exceptions.JedisValidationException;
/**
* Immutable class representing driver information for Redis client identification.
* <p>
* This class is used to identify the client library and any upstream drivers (such as Spring Data
* Redis or Spring Session) when connecting to Redis. The information is sent via the
* {@code CLIENT SETINFO} command.
* <p>
* The formatted name follows the pattern: {@code name(driver1_vVersion1;driver2_vVersion2)}
* @see ClientSetInfoConfig
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
public final class DriverInfo {
/**
* Set of brace characters that are not allowed in driver names or versions. These characters are
* used to delimit the driver information in the formatted output and would break parsing.
*/
private static final Set<Character> BRACES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList('(', ')', '[', ']', '{', '}')));
private final String name;
private final List<String> upstreamDrivers;
private DriverInfo(String name, List<String> upstreamDrivers) {
this.name = name;
this.upstreamDrivers = Collections.unmodifiableList(upstreamDrivers);
}
/**
* Creates a new {@link Builder} with default values.
* <p>
* The default name is "Jedis" (from {@link JedisMetaInfo#getArtifactId()}).
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a new {@link Builder} initialized with values from an existing {@link DriverInfo}.
* @param driverInfo the existing driver info to copy from, must not be {@code null}
* @return a new builder instance initialized with the existing values
* @throws JedisValidationException if driverInfo is {@code null}
*/
public static Builder builder(DriverInfo driverInfo) {
if (driverInfo == null) {
throw new JedisValidationException("DriverInfo must not be null");
}
return new Builder(driverInfo);
}
/**
* Returns the formatted name including upstream drivers or legacy suffix.
* <p>
* If a legacy suffix is set, returns the name followed by the suffix in parentheses. Otherwise,
* if upstream drivers are present, returns the name followed by upstream drivers in parentheses,
* separated by semicolons. If neither is set, returns just the name.
* <p>
* Examples:
* <ul>
* <li>{@code "jedis"} - no upstream drivers or suffix</li>
* <li>{@code "jedis(my-suffix)"} - legacy suffix mode</li>
* <li>{@code "jedis(spring-data-redis_v3.2.0)"} - one upstream driver</li>
* <li>{@code "jedis(spring-session_v3.3.0;spring-data-redis_v3.2.0)"} - multiple upstream
* drivers</li>
* </ul>
* @return the formatted name for use in CLIENT SETINFO
*/
public String getFormattedName() {
if (upstreamDrivers.isEmpty()) {
return name;
}
return String.format("%s(%s)", name, String.join(";", upstreamDrivers));
}
/**
* Returns the base library name without upstream driver information.
* @return the library name
*/
public String getName() {
return name;
}
/**
* Returns the formatted upstream drivers string (without the base library name).
* <p>
* Multiple drivers are separated by semicolons, with the most recently added driver appearing
* first.
* <p>
* Examples:
* <ul>
* <li>{@code "spring-data-redis_v3.2.0"} - single upstream driver</li>
* <li>{@code "spring-session_v3.3.0;spring-data-redis_v3.2.0"} - multiple upstream drivers</li>
* </ul>
* @return the formatted upstream drivers string, or {@code null} if no upstream drivers are set
*/
public String getUpstreamDrivers() {
if (upstreamDrivers.isEmpty()) {
return "";
}
return String.join(";", upstreamDrivers);
}
@Override
public String toString() {
return getFormattedName();
}
/**
* Builder for creating {@link DriverInfo} instances.
*/
public static class Builder {
private String name;
private final List<String> upstreamDrivers;
private Builder() {
this.name = JedisMetaInfo.getArtifactId();
this.upstreamDrivers = new ArrayList<>();
}
private Builder(DriverInfo driverInfo) {
this.name = driverInfo.name;
this.upstreamDrivers = new ArrayList<>(driverInfo.upstreamDrivers);
}
/**
* Sets the base library name.
* <p>
* This overrides the default name ("Jedis"). Use this when you want to completely customize the
* library identification.
* @param name the library name, must not be {@code null}
* @return this builder
* @throws JedisValidationException if name is {@code null}
*/
public Builder name(String name) {
if (name == null) {
throw new JedisValidationException("Name must not be null");
}
this.name = name;
return this;
}
/**
* Adds an upstream driver to the driver information.
* <p>
* Upstream drivers are prepended to the list, so the most recently added driver appears first
* in the formatted output.
* <p>
* The driver name must follow Maven artifactId naming conventions: lowercase letters, digits,
* hyphens, and underscores only, starting with a lowercase letter. Dots are only allowed after
* digits (for Scala cross-version naming like akka-redis_2.13).
* <p>
* Both values must not contain spaces, newlines, non-printable characters, or brace characters
* as these would violate the format of the Redis CLIENT LIST reply.
* @param driverName the name of the upstream driver (e.g., "spring-data-redis"), must not be
* {@code null}
* @param driverVersion the version of the upstream driver (e.g., "3.2.0"), must not be
* {@code null}
* @return this builder
* @throws JedisValidationException if the driver name or version is {@code null} or has invalid
* format
* @see <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven
* Naming Conventions</a>
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
public Builder addUpstreamDriver(String driverName, String driverVersion) {
if (driverName == null) {
throw new JedisValidationException("Driver name must not be null");
}
if (driverVersion == null) {
throw new JedisValidationException("Driver version must not be null");
}
validateDriverField(driverName, "Driver name");
validateDriverField(driverVersion, "Driver version");
String formattedDriverInfo = formatDriverInfo(driverName, driverVersion);
this.upstreamDrivers.add(0, formattedDriverInfo);
return this;
}
public Builder addUpstreamDriver(String driverName) {
if (driverName == null) {
throw new JedisValidationException("Driver name must not be null");
}
validateDriverField(driverName, "Driver name");
this.upstreamDrivers.add(0, driverName);
return this;
}
/**
* Builds and returns a new immutable {@link DriverInfo} instance.
* @return a new DriverInfo instance
*/
public DriverInfo build() {
return new DriverInfo(name, upstreamDrivers);
}
}
/**
* Validates that the value does not contain characters that would violate the format of the Redis
* CLIENT LIST reply.
* <p>
* Only printable ASCII characters (0x21-0x7E, i.e., '!' to '~') are allowed, excluding braces.
* @param value the value to validate
* @param fieldName the name of the field for error messages (e.g., "Driver name", "Driver
* version")
* @throws JedisValidationException if the value is empty or contains invalid characters
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
private static void validateDriverField(String value, String fieldName) {
if (value.trim().isEmpty()) {
throw new JedisValidationException(fieldName + " must not be empty");
}
validateNoInvalidCharacters(value, fieldName);
}
/**
* Validates that the value does not contain characters that would violate the format of the Redis
* CLIENT LIST reply: non-printable characters, spaces, or brace characters.
* <p>
* Only printable ASCII characters (0x21-0x7E, i.e., '!' to '~') are allowed, excluding braces.
* @param value the value to validate
* @param fieldName the name of the field for error messages
* @throws JedisValidationException if the value contains invalid characters
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
private static void validateNoInvalidCharacters(String value, String fieldName) {
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c < '!' || c > '~' || BRACES.contains(c)) {
throw new JedisValidationException(
fieldName + " must not contain spaces, newlines, non-printable characters, or braces");
}
}
}
private static String formatDriverInfo(String driverName, String driverVersion) {
return driverName + "_v" + driverVersion;
}
}