Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allow cache configuration from file #3059

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
*/
package org.springframework.data.redis.cache;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.io.FileNotFoundException;

import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.SimpleKey;
Expand All @@ -42,6 +47,7 @@
* @author Christoph Strobl
* @author Mark Paluch
* @author John Blum
* @author Chaelin Kwon
* @since 2.0
*/
public class RedisCacheConfiguration {
Expand Down Expand Up @@ -120,6 +126,106 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c
conversionService);
}

private static final Logger logger = Logger.getLogger(RedisCacheConfiguration.class.getName());

public static RedisCacheConfiguration propertyCacheConfig(@Nullable ClassLoader classLoader) {

DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);

Properties properties = loadProperties();

Duration ttl = getValidatedDuration(properties, "ttl", Duration.ofSeconds(0));
boolean cacheNullValues = getValidatedBoolean(properties, "nullValues", true);
String keyPrefix = getValidatedString(properties, "keyPrefix", "simple", new String[]{"simple", "none"});
SerializationPair<?> defaultSerializer = getValidatedSerializer(properties, "serializer", "raw", classLoader);
SerializationPair<String> keySerializer = getValidatedSerializer(properties, "key.serializer", "string", classLoader);
SerializationPair<?> valueSerializer = getValidatedSerializer(properties, "value.serializer", defaultSerializer, classLoader);

return new RedisCacheConfiguration(
TtlFunction.just(ttl),
cacheNullValues,
DEFAULT_ENABLE_TIME_TO_IDLE_EXPIRATION,
DEFAULT_USE_PREFIX,
CacheKeyPrefix.prefixed(keyPrefix),
keySerializer,
valueSerializer,
conversionService
);
}

private static Properties loadProperties() {
Properties properties = new Properties();
try (InputStream input = RedisCacheConfiguration.class.getClassLoader().getResourceAsStream("redis-cache.properties")) {
if (input != null) {
properties.load(input);
} else {
throw new FileNotFoundException("Property file 'redis-cache.properties' not found in the classpath");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load redis-cache.properties", e);
}
return properties;
}

private static Duration getValidatedDuration(Properties properties, String key, Duration defaultValue) {
String value = properties.getProperty(key, String.valueOf(defaultValue.getSeconds()));
try {
return Duration.ofSeconds(Long.parseLong(value));
} catch (NumberFormatException e) {
logger.warning("Invalid " + key + " value: " + value + ". Expected a positive integer. Defaulting to " + defaultValue);
return defaultValue;
}
}

private static boolean getValidatedBoolean(Properties properties, String key, boolean defaultValue) {
String value = properties.getProperty(key, String.valueOf(defaultValue));
switch (value.toLowerCase()) {
case "true":
case "false":
return Boolean.parseBoolean(value);
default:
logger.warning("Invalid " + key + " value: " + value + ". Expected 'true' or 'false'. Defaulting to " + defaultValue);
return defaultValue;
}
}

private static String getValidatedString(Properties properties, String key, String defaultValue, String[] validValues) {
String value = properties.getProperty(key, defaultValue);
for (String validValue : validValues) {
if (validValue.equalsIgnoreCase(value)) {
return value.toLowerCase();
}
}
logger.warning("Invalid " + key + " value: " + value + ". Expected one of " + String.join(", ", validValues) + ". Defaulting to " + defaultValue);
return defaultValue;
}

private static <T> SerializationPair<T> getValidatedSerializer(Properties properties, String key, Object defaultValue, @Nullable ClassLoader classLoader) {
String value = properties.getProperty(key);

if (value == null && defaultValue instanceof SerializationPair) {
return (SerializationPair<T>) defaultValue;
}
if (value == null) {
value = String.valueOf(defaultValue);
}

switch (value.toLowerCase()) {
case "java":
return (SerializationPair<T>) SerializationPair.fromSerializer(RedisSerializer.java(classLoader));
case "string":
return (SerializationPair<T>) SerializationPair.fromSerializer(RedisSerializer.string());
case "json":
return (SerializationPair<T>) SerializationPair.fromSerializer(RedisSerializer.json());
case "raw":
return (SerializationPair<T>) SerializationPair.fromSerializer(RedisSerializer.byteArray());
default:
logger.warning("Invalid " + key + " value: " + value + ". Expected 'raw', 'java', 'string', or 'json'. Defaulting to 'raw'.");
return (SerializationPair<T>) SerializationPair.fromSerializer(RedisSerializer.byteArray());
}
}

private final boolean cacheNullValues;
private final boolean enableTimeToIdle;
private final boolean usePrefix;
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/redis-cache.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ttl=100
nullValues=false
keyPrefix=none
serializer=json
key.serializer=string
value.serializer=json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.springframework.data.redis.cache;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
Expand All @@ -24,12 +25,17 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.util.Properties;

import org.junit.jupiter.api.Test;

import org.springframework.beans.DirectFieldAccessor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import org.springframework.lang.Nullable;

Expand All @@ -38,6 +44,7 @@
*
* @author Mark Paluch
* @author John Blum
* @author Chaelin Kwon
*/
class RedisCacheConfigurationUnitTests {

Expand Down Expand Up @@ -128,4 +135,65 @@ public String convert(DomainType source) {
return null;
}
}

@Test // DATAREDIS-938
void getCacheConfigFromProperties() {

RedisCacheConfiguration config = RedisCacheConfiguration.propertyCacheConfig(null);

Properties properties = new Properties();
try (InputStream input = RedisCacheConfiguration.class.getClassLoader().getResourceAsStream("redis-cache.properties")) {
if (input != null) {
properties.load(input);
} else {
throw new FileNotFoundException("Property file 'redis-cache.properties' not found in the classpath");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load redis-cache.properties", e);
}

assertThat(config.getTtlFunction().getTimeToLive(null, null)).isEqualTo(Duration.ofSeconds(Long.parseLong(properties.getProperty("ttl", "0"))));
assertThat(config.usePrefix()).isTrue();
assertThat(config.getKeyPrefixFor("myCache")).isEqualTo(properties.getProperty("keyPrefix", "") + "myCache::");
assertThat(config.getAllowCacheNullValues()).isEqualTo(Boolean.parseBoolean(properties.getProperty("nullValues", "true")));
}

@Test // DATAREDIS-938
void testPropertiesFileNonExist() {
assertThatThrownBy(() -> {
try (InputStream input = RedisCacheConfiguration.class.getClassLoader().getResourceAsStream("nonexistent-file.properties")) {
if (input == null) {
throw new FileNotFoundException("Property file 'nonexistent-file.properties' not found in the classpath");
}
Properties properties = new Properties();
properties.load(input);
} catch (Exception e) {
throw new RuntimeException("Failed to load properties", e);
}
}).isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to load properties");
}

@Test // DATAREDIS-938
void getInvalidFromProperties() {

RedisCacheConfiguration config = RedisCacheConfiguration.propertyCacheConfig(null);

Properties properties = new Properties();
try (InputStream input = RedisCacheConfiguration.class.getClassLoader().getResourceAsStream("redis-cache.properties")) {
if (input != null) {
properties.load(input);
} else {
throw new FileNotFoundException("Property file 'redis-cache.properties' not found in the classpath");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load redis-cache.properties", e);
}

assertThat(config.getTtlFunction().getTimeToLive(null, null)).isEqualTo(Duration.ZERO); //
assertThat(config.usePrefix()).isTrue();
assertThat(config.getKeyPrefixFor("myCache")).isEqualTo(properties.getProperty("keyPrefix", "") + "myCache::");
assertThat(config.getAllowCacheNullValues()).isEqualTo(Boolean.parseBoolean(properties.getProperty("nullValues", "true")));
}

}