diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java index 0d32f86a0d..3e7d7f60c8 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -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; @@ -42,6 +47,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Chaelin Kwon * @since 2.0 */ public class RedisCacheConfiguration { @@ -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 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 SerializationPair getValidatedSerializer(Properties properties, String key, Object defaultValue, @Nullable ClassLoader classLoader) { + String value = properties.getProperty(key); + + if (value == null && defaultValue instanceof SerializationPair) { + return (SerializationPair) defaultValue; + } + if (value == null) { + value = String.valueOf(defaultValue); + } + + switch (value.toLowerCase()) { + case "java": + return (SerializationPair) SerializationPair.fromSerializer(RedisSerializer.java(classLoader)); + case "string": + return (SerializationPair) SerializationPair.fromSerializer(RedisSerializer.string()); + case "json": + return (SerializationPair) SerializationPair.fromSerializer(RedisSerializer.json()); + case "raw": + return (SerializationPair) SerializationPair.fromSerializer(RedisSerializer.byteArray()); + default: + logger.warning("Invalid " + key + " value: " + value + ". Expected 'raw', 'java', 'string', or 'json'. Defaulting to 'raw'."); + return (SerializationPair) SerializationPair.fromSerializer(RedisSerializer.byteArray()); + } + } + private final boolean cacheNullValues; private final boolean enableTimeToIdle; private final boolean usePrefix; diff --git a/src/main/resources/redis-cache.properties b/src/main/resources/redis-cache.properties new file mode 100644 index 0000000000..153a4e1857 --- /dev/null +++ b/src/main/resources/redis-cache.properties @@ -0,0 +1,6 @@ +ttl=100 +nullValues=false +keyPrefix=none +serializer=json +key.serializer=string +value.serializer=json diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java index edbe99e964..d659d30600 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java @@ -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; @@ -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; @@ -38,6 +44,7 @@ * * @author Mark Paluch * @author John Blum + * @author Chaelin Kwon */ class RedisCacheConfigurationUnitTests { @@ -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"))); + } + }