diff --git a/build.gradle.kts b/build.gradle.kts index 90f0258..d7b6f12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { implementation(libs.spring.boot.starter.validation) implementation(libs.spring.boot.starter.redis) implementation(libs.spring.boot.starter.cache) - implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.jpa) implementation(libs.mybatis.starter.core) implementation(libs.flyway.core) @@ -52,9 +51,7 @@ dependencies { implementation(libs.jackson.jsr310) testImplementation(libs.spring.boot.starter.test) testImplementation(libs.reactor.test) - testImplementation(libs.spring.security.test) testImplementation(libs.mybatis.starter.test) - // runtimeOnly(libs.postgres.driver) runtimeOnly(libs.mysql.driver) testRuntimeOnly(libs.h2.database) testRuntimeOnly(libs.junit.launcher) diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/CacheConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/CacheConfig.java new file mode 100644 index 0000000..0502c4a --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/CacheConfig.java @@ -0,0 +1,102 @@ +package com.onixbyte.deltaforceguide.config; + +import com.onixbyte.deltaforceguide.shared.JacksonRedisSerialiser; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.time.Duration; + +/** + * Configuration class for Redis-based caching components. + *

+ * This configuration class provides beans for Redis cache management and template operations + * within the Helix application. It configures custom serialisation strategies using + * {@link GenericJackson2JsonRedisSerializer} for values and string serialisation for keys, + * ensuring optimal performance and compatibility with JSON-based data structures. + *

+ * The configuration includes: + *

+ * + * @author zihluwang + * @see RedisCacheManager + * @see RedisTemplate + * @see GenericJackson2JsonRedisSerializer + * @since 1.0.0 + */ +@Configuration +@EnableCaching +public class CacheConfig { + + /** + * Creates a custom Redis cache manager with JSON serialisation support. + *

+ * This method configures a {@link RedisCacheManager} that uses string serialisation for cache + * keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that + * complex objects can be stored and retrieved from Redis cache whilst maintaining readability + * and compatibility with JSON-based systems. + * + * @param connectionFactory the Redis connection factory used to establish connections + * @return a configured {@link RedisCacheManager} with custom serialisation settings + * @see RedisCacheManager + * @see GenericJackson2JsonRedisSerializer + * @see RedisSerializationContext + */ + @Bean + public RedisCacheManager cacheManager( + RedisConnectionFactory connectionFactory + ) { + var _keySerializer = RedisSerializer.string(); + + var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair + .fromSerializer(_keySerializer)) + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(JacksonRedisSerialiser.INSTANCE)) + .entryTtl(Duration.ofMinutes(90L)); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(connectionFactory) + .cacheDefaults(cacheConfiguration) + .build(); + } + + /** + * Creates a Redis template for direct Redis operations with custom serialisation. + *

+ * This method configures a {@link RedisTemplate} that uses string serialisation for keys + * and {@link GenericJackson2JsonRedisSerializer} for values. This template provides low-level + * access to Redis operations whilst ensuring consistent serialisation strategies across + * the application. + *

+ * The template is fully configured and ready for use after bean creation. + * + * @param connectionFactory the Redis connection factory used to establish connections + * @return a fully configured {@link RedisTemplate} for Redis operations + * @see RedisTemplate + * @see GenericJackson2JsonRedisSerializer + * @see RedisSerializer + */ + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory connectionFactory + ) { + var redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(RedisSerializer.string()); + redisTemplate.setValueSerializer(JacksonRedisSerialiser.INSTANCE); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java new file mode 100644 index 0000000..565bf69 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java @@ -0,0 +1,43 @@ +package com.onixbyte.deltaforceguide.config; + +import com.onixbyte.deltaforceguide.properties.CorsProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Optional; +import java.util.stream.Stream; + +@Configuration +@EnableConfigurationProperties({CorsProperties.class}) +public class CorsConfig implements WebMvcConfigurer { + + private final CorsProperties properties; + + public CorsConfig(CorsProperties properties) { + this.properties = properties; + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(properties.allowedOrigins()) + .allowedHeaders(properties.allowedHeaders()) + .allowedMethods(toHttpMethodNames(properties.allowedMethods())) + .allowCredentials(properties.allowCredentials()) + .maxAge(properties.maxAge().toSeconds()) + .exposedHeaders(properties.exposedHeaders()); + } + + private static String[] toHttpMethodNames(HttpMethod[] methods) { + return Optional.ofNullable(methods) + .stream() + .flatMap(Stream::of) + .map(HttpMethod::name) + .toList() + .toArray(String[]::new); + } + +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java new file mode 100644 index 0000000..a64a1ea --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java @@ -0,0 +1,20 @@ +package com.onixbyte.deltaforceguide.config; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.onixbyte.deltaforceguide.shared.JacksonModules; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonCustomiser() { + return (builder) -> { + builder.modules(JacksonModules.DATE_TIME_MODULE); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + }; + } +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java new file mode 100644 index 0000000..61acbfc --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java @@ -0,0 +1,10 @@ +package com.onixbyte.deltaforceguide.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"}) +public class MyBatisConfig { +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java new file mode 100644 index 0000000..44e3a4c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java @@ -0,0 +1,10 @@ +package com.onixbyte.deltaforceguide.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"}) +public class SpringDataConfig { +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java new file mode 100644 index 0000000..ff064d5 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java @@ -0,0 +1,24 @@ +package com.onixbyte.deltaforceguide.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.http.HttpMethod; + +import java.time.Duration; + +@ConfigurationProperties(prefix = "app.cors") +public record CorsProperties( + @DefaultValue({"Content-Type", "Authorization"}) + String[] allowedHeaders, + @DefaultValue({"GET", "POST", "PUT", "PATCH", "DELETE"}) + HttpMethod[] allowedMethods, + String[] allowedOrigins, + boolean allowCredentials, + boolean allowPrivateNetwork, + @DefaultValue("PT2H") + Duration maxAge, + @DefaultValue({"Content-Type", "Authorization"}) + String[] exposedHeaders +) { +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/DateTimeFormatters.java b/src/main/java/com/onixbyte/deltaforceguide/shared/DateTimeFormatters.java new file mode 100644 index 0000000..134408b --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/DateTimeFormatters.java @@ -0,0 +1,46 @@ +package com.onixbyte.deltaforceguide.shared; + +import java.time.format.DateTimeFormatter; + +/** + * Utility class providing predefined {@link DateTimeFormatter} instances for common date and + * time patterns. These formatters can be used to parse and format {@code java.time} objects + * consistently throughout an application. + * + * @author zihluwang + */ +public class DateTimeFormatters { + + /** + * A {@link DateTimeFormatter} for formatting and parsing full date and time with seconds, using + * the pattern "yyyy-MM-dd HH:mm:ss". + *

+ * Example: "{@code 2023-10-27 15:30:45}" + */ + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing dates only, using the + * pattern "yyyy-MM-dd". + *

+ * Example: "{@code 2023-10-27}" + */ + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing times only, using the + * pattern "HH:mm:ss". + *

+ * Example: "{@code 15:30:45}" + */ + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing year and month only, using the + * pattern "yyyy-MM". + *

+ * Example: "{@code 2023-10}" + */ + public static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java new file mode 100644 index 0000000..36332aa --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java @@ -0,0 +1,34 @@ +package com.onixbyte.deltaforceguide.shared; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class JacksonModules { + + public static final SimpleModule DATE_TIME_MODULE = initialiseDateTimeModule(); + + private static SimpleModule initialiseDateTimeModule() { + var javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatters.DATE_TIME_FORMATTER)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatters.DATE_TIME_FORMATTER)); + + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatters.DATE_FORMATTER)); + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatters.DATE_FORMATTER)); + + javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatters.TIME_FORMATTER)); + javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatters.TIME_FORMATTER)); + + return javaTimeModule; + } +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java new file mode 100644 index 0000000..5a300ba --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java @@ -0,0 +1,22 @@ +package com.onixbyte.deltaforceguide.shared; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; + +public class JacksonRedisSerialiser { + + public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer(); + + private static GenericJackson2JsonRedisSerializer initialiseSerializer() { + var serializer = new GenericJackson2JsonRedisSerializer(); + + serializer.configure((configurer) -> { + configurer.registerModule(new JavaTimeModule()); + configurer.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + }); + + return serializer; + } +} +