Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 150 additions & 4 deletions src/main/java/io/lettuce/core/RedisURI.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,24 @@ public class RedisURI implements Serializable, ConnectionPoint {

public static final Duration DEFAULT_TIMEOUT_DURATION = Duration.ofSeconds(DEFAULT_TIMEOUT);

/**
* Regex pattern for driver name validation. The name must start with a lowercase letter and contain only lowercase letters,
* digits, hyphens, and underscores. Mostly follows Maven artifactId naming conventions but also allows underscores.
*
* @see <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven Naming Conventions</a>
*/
private static final String DRIVER_NAME_PATTERN = "^[a-z][a-z0-9_-]*$";

/**
* Official semver.org regex pattern for semantic versioning validation.
*
* @see <a href="https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string">semver.org
* regex</a>
*/
private static final String SEMVER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"
+ "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";

private String host;

private String socket;
Expand All @@ -239,9 +257,11 @@ public class RedisURI implements Serializable, ConnectionPoint {

private String libraryName = LettuceVersion.getName();

private String upstreamDrivers;

private String libraryVersion = LettuceVersion.getVersion();

private RedisCredentialsProvider credentialsProvider = new StaticCredentialsProvider(null, null);;
private RedisCredentialsProvider credentialsProvider = new StaticCredentialsProvider(null, null);

private boolean ssl = false;

Expand Down Expand Up @@ -606,13 +626,19 @@ public void setClientName(String clientName) {
}

/**
* Returns the library name.
* Returns the library name to be sent via {@code CLIENT SETINFO}.
* <p>
* If upstream drivers have been added via {@link #addUpstreamDriver(String, String)}, the returned value will include them
* in the format: {@code libraryName(driver1_v1.0.0;driver2_v2.0.0)}. Otherwise, returns just the library name.
*
* @return the library name.
* @return the library name, potentially including upstream driver information.
* @since 6.3
*/
public String getLibraryName() {
return libraryName;
if (upstreamDrivers == null) {
return libraryName;
}
return libraryName + "(" + upstreamDrivers + ")";
}

/**
Expand All @@ -628,6 +654,79 @@ public void setLibraryName(String libraryName) {
this.libraryName = libraryName;
}

/**
* Adds an upstream driver to be appended to the library name when sent via {@code CLIENT SETINFO}.
* <p>
* This method allows upstream libraries (e.g., Spring Data Redis) that use Lettuce as their Redis driver to identify
* themselves. Multiple upstream drivers can be added and will be formatted according to the Redis CLIENT SETINFO format:
* {@code lettuce(driver1_v1.0.0;driver2_v2.0.0)}.
* <p>
* Each newly added upstream driver is prepended to the list, so the most recently added driver appears first. For example,
* if you call {@code addUpstreamDriver("spring-data-redis", "3.2.0")} followed by
* {@code addUpstreamDriver("spring-boot", "3.3.0")}, the resulting library name will be
* {@code lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)}.
* <p>
* The driver name must follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId
* naming conventions</a>: lowercase letters, digits, hyphens, and underscores only, starting with a lowercase letter (e.g.,
* {@code spring-data-redis}, {@code lettuce-core}, {@code akka-redis_2.13}).
* <p>
* The driver version must follow <a href="https://semver.org/">semantic versioning</a> (e.g., {@code 1.0.0},
* {@code 2.1.3-beta}, {@code 1.0.0-alpha.1}, {@code 1.0.0+build.123}).
*
* @param driverName the name of the upstream driver (e.g., "spring-data-redis"), must not be {@code null} and must follow
* <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId naming
* conventions</a>
* @param driverVersion the version of the upstream driver (e.g., "3.2.0"), must not be {@code null} and must follow
* <a href="https://semver.org/">semantic versioning</a>
* @throws IllegalArgumentException if the driver name or version format is invalid
* @since 6.5
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
public void addUpstreamDriver(String driverName, String driverVersion) {

LettuceAssert.notNull(driverName, "Upstream driver name must not be null");
LettuceAssert.notNull(driverVersion, "Upstream driver version must not be null");
validateDriverName(driverName);
validateDriverVersion(driverVersion);

String driver = driverName + "_v" + driverVersion;
this.upstreamDrivers = this.upstreamDrivers == null ? driver : driver + ";" + this.upstreamDrivers;
}

/**
* Validates that the driver name follows Maven artifactId naming conventions: lowercase letters, digits, hyphens, and
* underscores only, starting with a lowercase letter (e.g., {@code spring-data-redis}, {@code lettuce-core},
* {@code akka-redis_2.13}).
*
* @param driverName the driver name to validate
* @throws IllegalArgumentException if the driver name does not follow the expected naming conventions
* @see <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven Naming Conventions</a>
*/
private static void validateDriverName(String driverName) {
if (!driverName.matches(DRIVER_NAME_PATTERN)) {
throw new IllegalArgumentException(
"Upstream driver name must follow Maven artifactId naming conventions: lowercase letters, digits, hyphens, and underscores only (e.g., 'spring-data-redis', 'lettuce-core')");
}
}

/**
* Validates that the driver version follows semantic versioning (semver.org). The version must be in the format
* {@code MAJOR.MINOR.PATCH} with optional pre-release and build metadata suffixes.
* <p>
* Examples of valid versions: {@code 1.0.0}, {@code 2.1.3}, {@code 1.0.0-alpha}, {@code 1.0.0-alpha.1},
* {@code 1.0.0-0.3.7}, {@code 1.0.0-x.7.z.92}, {@code 1.0.0+20130313144700}, {@code 1.0.0-beta+exp.sha.5114f85}
*
* @param driverVersion the driver version to validate
* @throws IllegalArgumentException if the driver version does not follow semantic versioning
* @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
*/
private static void validateDriverVersion(String driverVersion) {
if (!driverVersion.matches(SEMVER_PATTERN)) {
throw new IllegalArgumentException(
"Upstream driver version must follow semantic versioning (e.g., '1.0.0', '2.1.3-beta', '1.0.0+build.123')");
}
}

/**
* Returns the library version.
*
Expand Down Expand Up @@ -1295,6 +1394,8 @@ public static class Builder {

private String libraryVersion = LettuceVersion.getVersion();

private String upstreamDrivers;

private RedisCredentialsProvider credentialsProvider;

private boolean ssl = false;
Expand Down Expand Up @@ -1630,6 +1731,50 @@ public Builder withLibraryName(String libraryName) {
return this;
}

/**
* Adds an upstream driver to be appended to the library name when sent via {@code CLIENT SETINFO}.
* <p>
* This method allows upstream libraries (e.g., Spring Data Redis) that use Lettuce as their Redis driver to identify
* themselves. Multiple upstream drivers can be added and will be formatted according to the Redis CLIENT SETINFO
* format: {@code lettuce(driver1_v1.0.0;driver2_v2.0.0)}.
* <p>
* Each newly added upstream driver is prepended to the list, so the most recently added driver appears first. For
* example, if you call {@code addUpstreamDriver("spring-data-redis", "3.2.0")} followed by
* {@code addUpstreamDriver("spring-boot", "3.3.0")}, the resulting library name will be
* {@code lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)}.
* <p>
* The driver name must follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven
* artifactId naming conventions</a>: lowercase letters, digits, hyphens, and underscores only, starting with a
* lowercase letter (e.g., {@code spring-data-redis}, {@code lettuce-core}, {@code akka-redis_2.13}).
* <p>
* The driver version must follow <a href="https://semver.org/">semantic versioning</a> (e.g., {@code 1.0.0},
* {@code 2.1.3-beta}, {@code 1.0.0-alpha.1}, {@code 1.0.0+build.123}).
* <p>
* Also sets upstream driver for already configured Redis Sentinel nodes.
*
* @param driverName the name of the upstream driver (e.g., "spring-data-redis"), must not be {@code null} and must
* follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId naming
* conventions</a>
* @param driverVersion the version of the upstream driver (e.g., "3.2.0"), must not be {@code null} and must follow
* <a href="https://semver.org/">semantic versioning</a>
* @return the builder
* @throws IllegalArgumentException if the driver name or version format is invalid
* @since 6.5
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
*/
public Builder addUpstreamDriver(String driverName, String driverVersion) {

LettuceAssert.notNull(driverName, "Upstream driver name must not be null");
LettuceAssert.notNull(driverVersion, "Upstream driver version must not be null");
validateDriverName(driverName);
validateDriverVersion(driverVersion);

String driver = driverName + "_v" + driverVersion;
this.upstreamDrivers = this.upstreamDrivers == null ? driver : driver + ";" + this.upstreamDrivers;
this.sentinels.forEach(it -> it.addUpstreamDriver(driverName, driverVersion));
return this;
}

/**
* Configures a library version. Sets library version also for already configured Redis Sentinel nodes.
*
Expand Down Expand Up @@ -1790,6 +1935,7 @@ public RedisURI build() {
redisURI.setClientName(clientName);
redisURI.setLibraryName(libraryName);
redisURI.setLibraryVersion(libraryVersion);
redisURI.upstreamDrivers = upstreamDrivers;

redisURI.setSentinelMasterId(sentinelMasterId);

Expand Down
71 changes: 70 additions & 1 deletion src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

/**
Expand Down Expand Up @@ -102,6 +101,76 @@ void redisWithLibrary() {
assertThat(result.getLibraryVersion()).isEqualTo("1.foo");
}

@Test
void redisWithSingleUpstreamDriver() {
RedisURI result = RedisURI.Builder.redis("localhost").withLibraryName("lettuce")
.addUpstreamDriver("spring-data-redis", "3.2.0").build();

assertThat(result.getLibraryName()).isEqualTo("lettuce(spring-data-redis_v3.2.0)");
}

@Test
void redisWithMultipleUpstreamDrivers() {
RedisURI result = RedisURI.Builder.redis("localhost").withLibraryName("lettuce")
.addUpstreamDriver("spring-data-redis", "3.2.0").addUpstreamDriver("spring-boot", "3.3.0").build();

// Most recently added driver should appear first (prepend behavior)
assertThat(result.getLibraryName()).isEqualTo("lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)");
}

@Test
void redisWithUpstreamDriverNullNameShouldFail() {
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver(null, "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisWithUpstreamDriverNullVersionShouldFail() {
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", null))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisWithUpstreamDriverNameWithSpacesShouldFail() {
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("spring data", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisWithUpstreamDriverVersionWithSpacesShouldFail() {
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "1.0 beta"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisWithUpstreamDriverInvalidNameFormatShouldFail() {
// Name starting with a number
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("123driver", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
// Name with @ symbol
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("driver@name", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
// Name starting with hyphen
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("-spring-data", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
// Name with dots (not allowed in Maven artifactId)
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("com.example.driver", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
// Name with uppercase letters
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("Spring-Data-Redis", "1.0.0"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisWithUpstreamDriverInvalidVersionFormatShouldFail() {
// Version without patch number
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "1.0"))
.isInstanceOf(IllegalArgumentException.class);
// Version with leading zeros
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "01.0.0"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void redisHostAndPortWithInvalidPort() {
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost", -1)).isInstanceOf(IllegalArgumentException.class);
Expand Down
Loading
Loading