Skip to content

Commit 5d885e2

Browse files
mp911dechristophstrobl
authored andcommitted
Refine RedisCollectionFactoryBean collection creation.
We now cross-check the existing key type against the specified CollectionType to avoid collection creation that doesn't match the configured CollectionType. If the existing key type doesn't match the configured CollectionType, collection creation fails with a validation error. Closes #2633 Original Pull Request: #2637
1 parent 9fe32d5 commit 5d885e2

File tree

2 files changed

+113
-33
lines changed

2 files changed

+113
-33
lines changed

src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java

+56-31
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
*/
1616
package org.springframework.data.redis.support.collections;
1717

18+
import java.util.function.Supplier;
19+
1820
import org.springframework.beans.factory.BeanNameAware;
1921
import org.springframework.beans.factory.InitializingBean;
2022
import org.springframework.beans.factory.SmartFactoryBean;
2123
import org.springframework.data.redis.connection.DataType;
24+
import org.springframework.data.redis.core.RedisOperations;
2225
import org.springframework.data.redis.core.RedisTemplate;
2326
import org.springframework.data.util.Lazy;
2427
import org.springframework.lang.Nullable;
@@ -27,60 +30,88 @@
2730

2831
/**
2932
* Factory bean that facilitates creation of Redis-based collections. Supports list, set, zset (or sortedSet), map (or
30-
* hash) and properties. Will use the key type if it exists or to create a dedicated collection (Properties vs Map).
31-
* Otherwise uses the provided type (default is list).
33+
* hash) and properties. Uses the key and {@link CollectionType} to determine what collection type to use. The factory
34+
* verifies the key type if a {@link CollectionType} is specified. Defaults to {@link CollectionType#LIST}.
3235
*
3336
* @author Costin Leau
3437
* @author Christoph Strobl
38+
* @author Mark Paluch
39+
* @see RedisStore
3540
*/
3641
public class RedisCollectionFactoryBean implements SmartFactoryBean<RedisStore>, BeanNameAware, InitializingBean {
3742

3843
/**
3944
* Collection types supported by this factory.
4045
*
4146
* @author Costin Leau
47+
* @author Mark Paluch
4248
*/
4349
public enum CollectionType {
4450
LIST {
4551

52+
@Override
4653
public DataType dataType() {
4754
return DataType.LIST;
4855
}
4956
},
5057
SET {
5158

59+
@Override
5260
public DataType dataType() {
5361
return DataType.SET;
5462
}
5563
},
5664
ZSET {
5765

66+
@Override
5867
public DataType dataType() {
5968
return DataType.ZSET;
6069
}
6170
},
6271
MAP {
6372

73+
@Override
6474
public DataType dataType() {
6575
return DataType.HASH;
6676
}
6777
},
6878
PROPERTIES {
6979

80+
@Override
7081
public DataType dataType() {
7182
return DataType.HASH;
7283
}
7384
};
7485

7586
abstract DataType dataType();
87+
88+
/**
89+
* Attempt to find a {@link CollectionType} by {@link DataType}. Defaults to {@link Supplier ifNotFound} when
90+
* {@code dataType} is {@literal null} or the collection type cannot be determined.
91+
*
92+
* @param dataType the {@link DataType} to look up.
93+
* @param ifNotFound supplier for a default value.
94+
* @since 3.2
95+
*/
96+
static CollectionType findCollectionType(@Nullable DataType dataType, Supplier<CollectionType> ifNotFound) {
97+
98+
for (CollectionType collectionType : values()) {
99+
if (collectionType.dataType() == dataType) {
100+
return collectionType;
101+
}
102+
}
103+
104+
return ifNotFound.get();
105+
}
76106
}
77107

78-
private @Nullable Lazy<RedisStore> store;
79-
private @Nullable CollectionType type = null;
108+
private @Nullable CollectionType type;
80109
private @Nullable RedisTemplate<String, ?> template;
81110
private @Nullable String key;
82111
private @Nullable String beanName;
83112

113+
private @Nullable Lazy<RedisStore> store;
114+
84115
@Override
85116
public void afterPropertiesSet() {
86117

@@ -93,46 +124,40 @@ public void afterPropertiesSet() {
93124

94125
store = Lazy.of(() -> {
95126

96-
DataType dt = template.type(key);
127+
DataType keyType = template.type(key);
97128

98129
// can't create store
99-
Assert.isTrue(!DataType.STRING.equals(dt), "Cannot create store on keys of type 'string'");
130+
Assert.isTrue(!DataType.STREAM.equals(keyType), "Cannot create store on keys of type 'STREAM'");
100131

101-
RedisStore tmp = createStore(dt);
132+
if (this.type == null) {
133+
this.type = CollectionType.findCollectionType(keyType, () -> CollectionType.LIST);
134+
}
102135

103-
if (tmp == null) {
104-
if (type == null) {
105-
type = CollectionType.LIST;
106-
}
107-
tmp = createStore(type.dataType());
136+
if (keyType != null && DataType.NONE != keyType && this.type.dataType() != keyType) {
137+
throw new IllegalArgumentException(
138+
String.format("Cannot create collection type '%s' for a key containing '%s'", this.type, keyType));
108139
}
109-
return tmp;
140+
141+
return createStore(this.type, key, template);
110142
});
111143
}
112144

113-
@SuppressWarnings("unchecked")
114-
private RedisStore createStore(DataType dt) {
115-
switch (dt) {
116-
case LIST:
117-
return RedisList.create(key, template);
118-
119-
case SET:
120-
return new DefaultRedisSet(key, template);
121-
122-
case ZSET:
123-
return RedisZSet.create(key, template);
145+
private RedisStore createStore(CollectionType collectionType, String key, RedisOperations<String, ?> operations) {
124146

125-
case HASH:
126-
if (CollectionType.PROPERTIES.equals(type)) {
127-
return new RedisProperties(key, template);
128-
}
129-
return new DefaultRedisMap(key, template);
130-
}
131-
return null;
147+
return switch (collectionType) {
148+
case LIST -> RedisList.create(key, operations);
149+
case SET -> new DefaultRedisSet<>(key, operations);
150+
case ZSET -> RedisZSet.create(key, operations);
151+
case PROPERTIES -> new RedisProperties(key, operations);
152+
case MAP -> new DefaultRedisMap<>(key, operations);
153+
};
132154
}
133155

134156
@Override
135157
public RedisStore getObject() {
158+
159+
Assert.state(store != null,
160+
"RedisCollectionFactoryBean is not initialized. Ensure to initialize this factory by calling afterPropertiesSet() before obtaining the factory object.");
136161
return store.get();
137162
}
138163

src/test/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBeanTests.java

+57-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.util.Map;
21+
2022
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.BeforeEach;
2124
import org.junit.jupiter.api.Test;
22-
2325
import org.springframework.data.redis.ObjectFactory;
2426
import org.springframework.data.redis.StringObjectFactory;
2527
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
@@ -30,7 +32,10 @@
3032
import org.springframework.data.redis.test.extension.RedisStanalone;
3133

3234
/**
35+
* Integration tests for {@link RedisCollectionFactoryBean}.
36+
*
3337
* @author Costin Leau
38+
* @author Mark Paluch
3439
*/
3540
public class RedisCollectionFactoryBeanTests {
3641

@@ -45,6 +50,12 @@ public class RedisCollectionFactoryBeanTests {
4550
this.template = new StringRedisTemplate(jedisConnFactory);
4651
}
4752

53+
@BeforeEach
54+
void setUp() {
55+
this.template.delete("key");
56+
this.template.delete("nosrt");
57+
}
58+
4859
@AfterEach
4960
void tearDown() throws Exception {
5061
// clean up the whole db
@@ -86,8 +97,30 @@ void testNone() throws Exception {
8697
assertThat(store).isInstanceOf(DefaultRedisList.class);
8798
}
8899

100+
@Test // GH-2633
101+
void testExisting() {
102+
103+
template.delete("key");
104+
template.opsForHash().put("key", "k", "v");
105+
106+
assertThat(createCollection("key")).isInstanceOf(DefaultRedisMap.class);
107+
assertThat(createCollection("key", CollectionType.MAP)).isInstanceOf(DefaultRedisMap.class);
108+
109+
template.delete("key");
110+
template.opsForSet().add("key", "1", "2");
111+
112+
assertThat(createCollection("key")).isInstanceOf(DefaultRedisSet.class);
113+
assertThat(createCollection("key", CollectionType.SET)).isInstanceOf(DefaultRedisSet.class);
114+
115+
template.delete("key");
116+
template.opsForList().leftPush("key", "1", "2");
117+
118+
assertThat(createCollection("key")).isInstanceOf(DefaultRedisList.class);
119+
assertThat(createCollection("key", CollectionType.LIST)).isInstanceOf(DefaultRedisList.class);
120+
}
121+
89122
@Test
90-
void testExistingCol() throws Exception {
123+
void testExistingCol() {
91124
String key = "set";
92125
String val = "value";
93126

@@ -102,6 +135,28 @@ void testExistingCol() throws Exception {
102135

103136
col = createCollection(key, CollectionType.PROPERTIES);
104137
assertThat(col).isInstanceOf(RedisProperties.class);
138+
}
139+
140+
@Test // GH-2633
141+
void testIncompatibleCollections() {
142+
143+
template.opsForValue().set("key", "value");
144+
assertThatIllegalArgumentException().isThrownBy(() -> createCollection("key", CollectionType.LIST))
145+
.withMessageContaining("Cannot create collection type 'LIST' for a key containing 'STRING'");
146+
147+
template.delete("key");
148+
template.opsForList().leftPush("key", "value");
149+
assertThatIllegalArgumentException().isThrownBy(() -> createCollection("key", CollectionType.SET))
150+
.withMessageContaining("Cannot create collection type 'SET' for a key containing 'LIST'");
151+
152+
}
153+
154+
@Test // GH-2633
155+
void shouldFailForStreamCreation() {
156+
157+
template.opsForStream().add("key", Map.of("k", "v"));
158+
assertThatIllegalArgumentException().isThrownBy(() -> createCollection("key", CollectionType.LIST))
159+
.withMessageContaining("Cannot create store on keys of type 'STREAM'");
105160

106161
}
107162
}

0 commit comments

Comments
 (0)