();
+
+ public void addCategory(Category category) {
+ categories.add(category);
+ }
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/BookRating.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/BookRating.java
new file mode 100644
index 000000000..f8df13875
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/BookRating.java
@@ -0,0 +1,38 @@
+package id.my.hendisantika.springbootredissample.model;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Builder;
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.redis.core.RedisHash;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.38
+ * To change this template use File | Settings | File Templates.
+ */
+@Data
+@Builder
+@RedisHash
+public class BookRating {
+ @Id
+ private String id;
+
+ @NotNull
+ @Reference
+ private User user;
+
+ @NotNull
+ @Reference
+ private Book book;
+
+ @NotNull
+ private Integer rating;
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Category.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Category.java
new file mode 100644
index 000000000..2f0856c8b
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Category.java
@@ -0,0 +1,32 @@
+package id.my.hendisantika.springbootredissample.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+import java.io.Serializable;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.38
+ * To change this template use File | Settings | File Templates.
+ */
+@Data
+@Builder
+@RedisHash
+@AllArgsConstructor
+@NoArgsConstructor
+public class Category implements Serializable {
+ @Id
+ private String id;
+ private String name;
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Role.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Role.java
new file mode 100644
index 000000000..b185bc108
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/Role.java
@@ -0,0 +1,52 @@
+package id.my.hendisantika.springbootredissample.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.38
+ * To change this template use File | Settings | File Templates.
+ */
+
+/**
+ * Represents a role entity stored in Redis.
+ *
+ * This class is annotated with {@link Builder} to provide a builder pattern for creating instances, and
+ * {@link Data} to generate getters, setters, equals, hashCode, and toString methods. The class is also annotated
+ * with {@link RedisHash} to indicate that it is a Redis hash stored in Redis.
+ */
+@Data
+@Builder
+@RedisHash
+@AllArgsConstructor
+@NoArgsConstructor
+public class Role {
+
+ /**
+ * The unique identifier for the role.
+ *
+ * This field is marked with {@link Id} to indicate it is the primary key in Redis.
+ */
+ @Id
+ private String id;
+
+ /**
+ * The name of the role.
+ *
+ * This field is indexed to allow for efficient querying by role name.
+ */
+ @Indexed
+ private String name;
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/User.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/User.java
new file mode 100644
index 000000000..542f752b1
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/model/User.java
@@ -0,0 +1,128 @@
+package id.my.hendisantika.springbootredissample.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.40
+ * To change this template use File | Settings | File Templates.
+ */
+
+/**
+ * Represents a user entity stored in Redis with associated attributes and behavior.
+ *
+ * This class is annotated with {@link EqualsAndHashCode} and {@link ToString} to include only
+ * explicitly specified fields in equality checks and string representation, respectively. It also
+ * uses the {@link Data} annotation to generate getters, setters, and other common methods.
+ *
+ *
+ * Each user has an ID, a name, an email, a password, and an optional password confirmation.
+ * Additionally, users can be associated with multiple roles.
+ *
+ *
+ * The class is annotated with {@link RedisHash} to indicate that it is a Redis hash stored in Redis.
+ */
+@Data
+@RedisHash
+@JsonIgnoreProperties(value = {"password", "passwordConfirm"}, allowSetters = true)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@ToString(onlyExplicitlyIncluded = true)
+@AllArgsConstructor
+@NoArgsConstructor
+public class User {
+
+ /**
+ * The unique identifier for the user.
+ *
+ * This field is marked with {@link Id} to indicate it is the primary key in Redis. It is also
+ * included in the string representation of the user.
+ */
+ @Id
+ @ToString.Include
+ private String id;
+
+ /**
+ * The name of the user.
+ *
+ * This field must be non-null and have a length between 2 and 48 characters. It is included in
+ * the string representation of the user.
+ */
+ @NotNull
+ @Size(min = 2, max = 48)
+ @ToString.Include
+ private String name;
+
+ /**
+ * The email address of the user.
+ *
+ * This field must be non-null, a valid email address, and is included in both the equality
+ * checks and string representation of the user. It is also indexed for faster lookups.
+ */
+ @Email
+ @NotNull
+ @EqualsAndHashCode.Include
+ @ToString.Include
+ @Indexed
+ private String email;
+
+ /**
+ * The password for the user.
+ *
+ * This field is required but not included in equality checks or the string representation of the
+ * user for security reasons.
+ */
+ @NotNull
+ private String password;
+
+ /**
+ * A confirmation password used for validation purposes.
+ *
+ * This field is transient and not persisted in Redis. It is used for password confirmation
+ * during user creation or updates.
+ */
+ @Transient
+ private String passwordConfirm;
+
+ /**
+ * The roles associated with the user.
+ *
+ * This field is a set of {@link Role} objects and is used to manage the user's roles. It is not
+ * included in equality checks or the string representation of the user. The default is an empty
+ * set.
+ */
+ @Reference
+ private Set roles = new HashSet();
+
+ /**
+ * Adds a role to the user.
+ *
+ * This method allows adding a {@link Role} to the user's set of roles.
+ *
+ * @param role the {@link Role} to be added
+ */
+ public void addRole(Role role) {
+ roles.add(role);
+ }
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRatingRepository.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRatingRepository.java
new file mode 100644
index 000000000..a89272405
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRatingRepository.java
@@ -0,0 +1,20 @@
+package id.my.hendisantika.springbootredissample.repository;
+
+import id.my.hendisantika.springbootredissample.model.BookRating;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.41
+ * To change this template use File | Settings | File Templates.
+ */
+@Repository
+public interface BookRatingRepository extends CrudRepository {
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRepository.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRepository.java
new file mode 100644
index 000000000..9a9a96855
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/BookRepository.java
@@ -0,0 +1,21 @@
+package id.my.hendisantika.springbootredissample.repository;
+
+import id.my.hendisantika.springbootredissample.model.Book;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.PagingAndSortingRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.41
+ * To change this template use File | Settings | File Templates.
+ */
+@Repository
+public interface BookRepository extends PagingAndSortingRepository, CrudRepository {
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/CategoryRepository.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/CategoryRepository.java
new file mode 100644
index 000000000..54f6c98e7
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/CategoryRepository.java
@@ -0,0 +1,20 @@
+package id.my.hendisantika.springbootredissample.repository;
+
+import id.my.hendisantika.springbootredissample.model.Category;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.41
+ * To change this template use File | Settings | File Templates.
+ */
+@Repository
+public interface CategoryRepository extends CrudRepository {
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/RoleRepository.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/RoleRepository.java
new file mode 100644
index 000000000..e735bcabb
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/RoleRepository.java
@@ -0,0 +1,38 @@
+package id.my.hendisantika.springbootredissample.repository;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.42
+ * To change this template use File | Settings | File Templates.
+ */
+
+import id.my.hendisantika.springbootredissample.model.Role;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Repository interface for {@link Role} entities.
+ *
+ * This interface extends {@link CrudRepository} to provide basic CRUD operations for {@link Role} entities.
+ * It also includes a custom query method to find a {@link Role} by its name.
+ */
+@Repository
+public interface RoleRepository extends CrudRepository {
+
+ /**
+ * Finds the first {@link Role} entity by its name.
+ *
+ * This method retrieves a {@link Role} entity where the name matches the specified role name.
+ * If multiple roles have the same name, only the first one is returned.
+ *
+ * @param role the name of the role to search for
+ * @return the first {@link Role} with the given name, or {@code null} if no role is found
+ */
+ Role findFirstByname(String role);
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/UserRepository.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/UserRepository.java
new file mode 100644
index 000000000..2bb563ace
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/java/id/my/hendisantika/springbootredissample/repository/UserRepository.java
@@ -0,0 +1,21 @@
+package id.my.hendisantika.springbootredissample.repository;
+
+import id.my.hendisantika.springbootredissample.model.User;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Created by IntelliJ IDEA.
+ * Project : spring-boot-redis-sample
+ * User: hendisantika
+ * Link: s.id/hendisantika
+ * Email: hendisantika@yahoo.co.id
+ * Telegram : @hendisantika34
+ * Date: 05/04/25
+ * Time: 07.42
+ * To change this template use File | Settings | File Templates.
+ */
+@Repository
+public interface UserRepository extends CrudRepository {
+ User findFirstByEmail(String email);
+}
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/application.properties b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/application.properties
new file mode 100644
index 000000000..58c226445
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+spring.application.name=bookstore
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+spring.data.redis.password=${REDIS_PASSWORD:53cret}
+spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
+app.numberOfRatings=5000
+app.ratingStars=5
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/books/_books.json b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/books/_books.json
new file mode 100644
index 000000000..e2813fba2
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/books/_books.json
@@ -0,0 +1,52 @@
+[
+ {
+ "id": "book123",
+ "title": "The Hitchhiker's Guide to the Galaxy",
+ "subtitle": "A Trilogy in Five Parts",
+ "description": "Seconds before the Earth is demolished to make way for a galactic freeway, Arthur Dent is plucked off the planet by his friend Ford Prefect, a researcher for the revised edition of The Hitchhiker's Guide to the Galaxy who, for the last fifteen years, has been posing as an out-of-work actor.",
+ "language": "English",
+ "pageCount": 224,
+ "thumbnail": "https://example.com/hitchhikers-guide-thumbnail.jpg",
+ "price": 12.99,
+ "currency": "USD",
+ "infoLink": "https://example.com/hitchhikers-guide-info",
+ "authors": [
+ "Douglas Adams"
+ ],
+ "categories": [
+ {
+ "id": "category_scifi",
+ "name": "Science Fiction"
+ },
+ {
+ "id": "category_humor",
+ "name": "Humor"
+ }
+ ]
+ },
+ {
+ "id": "book1234",
+ "title": "The Hitchhiker's Guide to the Galaxy",
+ "subtitle": "A Trilogy in Five Parts",
+ "description": "Seconds before the Earth is demolished to make way for a galactic freeway, Arthur Dent is plucked off the planet by his friend Ford Prefect, a researcher for the revised edition of The Hitchhiker's Guide to the Galaxy who, for the last fifteen years, has been posing as an out-of-work actor.",
+ "language": "English",
+ "pageCount": 212,
+ "thumbnail": "https://example.com/hitchhikers-guide-thumbnail.jpg",
+ "price": 12.99,
+ "currency": "USD",
+ "infoLink": "https://example.com/hitchhikers-guide-info",
+ "authors": [
+ "Douglas Adams"
+ ],
+ "categories": [
+ {
+ "id": "category_scifi",
+ "name": "Science Fiction"
+ },
+ {
+ "id": "category_humor",
+ "name": "Humor"
+ }
+ ]
+ }
+]
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/users/users.json b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/users/users.json
new file mode 100644
index 000000000..0891454aa
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/main/resources/data/users/users.json
@@ -0,0 +1,71 @@
+[
+ {
+ "id": "18c72298-80e6-401a-be63-eef50ed2f4c0",
+ "name": "Yuji Itadori",
+ "email": "yuji.itadori@jujutsukaisen.com",
+ "password": "hashedPasswordYuji",
+ "passwordConfirm": "hashedPasswordYuji",
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin"
+ }
+ ]
+ },
+ {
+ "id": "18c72298-80e6-401a-be63-eef50ed2f4c1",
+ "name": "Megumi Fushiguro",
+ "email": "megumi.fushiguro@jujutsukaisen.com",
+ "password": "hashedPasswordMegumi",
+ "passwordConfirm": "hashedPasswordMegumi",
+ "roles": [
+ {
+ "id": "customer",
+ "name": "customer"
+ }
+ ]
+ },
+ {
+ "id": "18c72298-80e6-401a-be63-eef50ed2f4c2",
+ "name": "Nobara Kugisaki",
+ "email": "nobara.kugisaki@jujutsukaisen.com",
+ "password": "hashedPasswordNobara",
+ "passwordConfirm": "hashedPasswordNobara",
+ "roles": [
+ {
+ "id": "customer",
+ "name": "customer"
+ }
+ ]
+ },
+ {
+ "id": "18c72298-80e6-401a-be63-eef50ed2f4c3",
+ "name": "Satoru Gojo",
+ "email": "satoru.gojo@jujutsukaisen.com",
+ "password": "hashedPasswordSatoru",
+ "passwordConfirm": "hashedPasswordSatoru",
+ "roles": [
+ {
+ "id": "customer",
+ "name": "customer"
+ }
+ ]
+ },
+ {
+ "id": "18c72298-80e6-401a-be63-eef50ed2f4c4",
+ "name": "Ryomen Sukuna",
+ "email": "ryomen.sukuna@jujutsukaisen.com",
+ "password": "hashedPasswordSukuna",
+ "passwordConfirm": "hashedPasswordSukuna",
+ "roles": [
+ {
+ "id": "customer",
+ "name": "customer"
+ },
+ {
+ "id": "customer",
+ "name": "customer"
+ }
+ ]
+ }
+]
diff --git a/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/test/java/id/my/hendisantika/springbootredissample/SpringBootRedisSampleApplicationTests.java b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/test/java/id/my/hendisantika/springbootredissample/SpringBootRedisSampleApplicationTests.java
new file mode 100644
index 000000000..ac8586e93
--- /dev/null
+++ b/jdk_21_maven/cs/rest/spring-boot-redis-sample/src/test/java/id/my/hendisantika/springbootredissample/SpringBootRedisSampleApplicationTests.java
@@ -0,0 +1,56 @@
+package id.my.hendisantika.springbootredissample;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@Testcontainers
+class SpringBootRedisSampleApplicationTests {
+
+ private static final String REDIS_IMAGE_NAME = "redis:7.0-alpine";
+ private static final int REDIS_PORT = 6379;
+ @Container
+ private static final GenericContainer> redis =
+ new GenericContainer<>(DockerImageName.parse(REDIS_IMAGE_NAME))
+ .withExposedPorts(REDIS_PORT);
+ @Autowired
+ private StringRedisTemplate redisTemplate;
+
+ @BeforeAll
+ static void beforeAll() {
+ // No need to start the container here, @Container will handle it
+ }
+
+ @AfterAll
+ static void afterAll() {
+ // No need to stop the container here, @Container will handle it
+ }
+
+ @DynamicPropertySource
+ static void redisProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.data.redis.host", redis::getHost);
+ registry.add("spring.data.redis.port", () -> redis.getMappedPort(REDIS_PORT));
+ }
+
+ @Test
+ void testSaveKeyValueAndGetValue() {
+ final String key = "testKey", value = "testValue";
+
+ redisTemplate.opsForValue().set(key, value);
+
+ assertThat(redisTemplate.opsForValue().get(key)).isEqualTo(value);
+ }
+
+}
diff --git a/jdk_21_maven/em/embedded/rest/pom.xml b/jdk_21_maven/em/embedded/rest/pom.xml
index 143794554..6d484f746 100644
--- a/jdk_21_maven/em/embedded/rest/pom.xml
+++ b/jdk_21_maven/em/embedded/rest/pom.xml
@@ -13,6 +13,7 @@
person-controller
+ spring-boot-redis-sample
diff --git a/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/pom.xml b/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/pom.xml
new file mode 100644
index 000000000..29876f75b
--- /dev/null
+++ b/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ evomaster-benchmark-jdk21-em-embedded-rest-spring-boot-redis-sample
+ jar
+
+
+ org.evomaster
+ evomaster-benchmark-jdk21-em-embedded-rest
+ 4.3.1-SNAPSHOT
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.1.0
+ pom
+ import
+
+
+
+
+
+
+ id.my.hendisantika
+ spring-boot-redis-sample
+ 0.0.1-SNAPSHOT
+
+
+ org.testcontainers
+ testcontainers
+ compile
+
+
+ junit
+ junit
+ compile
+ 4.11
+
+
+
+
\ No newline at end of file
diff --git a/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/src/main/java/em/embedded/id/my/hendisantika/springbootredissample/EmbeddedEvoMasterController.java b/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/src/main/java/em/embedded/id/my/hendisantika/springbootredissample/EmbeddedEvoMasterController.java
new file mode 100644
index 000000000..fb7c46dbc
--- /dev/null
+++ b/jdk_21_maven/em/embedded/rest/spring-boot-redis-sample/src/main/java/em/embedded/id/my/hendisantika/springbootredissample/EmbeddedEvoMasterController.java
@@ -0,0 +1,112 @@
+package em.embedded.id.my.hendisantika.springbootredissample;
+
+import id.my.hendisantika.springbootredissample.SpringBootRedisSampleApplication;
+import org.evomaster.client.java.controller.EmbeddedSutController;
+import org.evomaster.client.java.controller.InstrumentedSutStarter;
+import org.evomaster.client.java.controller.api.dto.SutInfoDto;
+import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto;
+import org.evomaster.client.java.controller.problem.ProblemInfo;
+import org.evomaster.client.java.controller.problem.RestProblem;
+import org.evomaster.client.java.sql.DbSpecification;
+import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.testcontainers.containers.GenericContainer;
+
+import java.util.List;
+import java.util.Map;
+
+public class EmbeddedEvoMasterController extends EmbeddedSutController {
+
+ private static final int REDIS_PORT = 6379;
+
+ private final GenericContainer> redis = new GenericContainer<>("redis:7.0")
+ .withExposedPorts(REDIS_PORT);
+
+ private ConfigurableApplicationContext ctx;
+ private String redisHost;
+ private int redisPort;
+
+ public static void main(String[] args) {
+ int port = 40100;
+ if (args.length > 0) port = Integer.parseInt(args[0]);
+ EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port);
+ InstrumentedSutStarter starter = new InstrumentedSutStarter(controller);
+ starter.start();
+ }
+
+ public EmbeddedEvoMasterController() { this(40100); }
+
+ public EmbeddedEvoMasterController(int port) { setControllerPort(port); }
+
+ @Override
+ public String startSut() {
+ redis.start();
+ redisHost = redis.getHost();
+ redisPort = redis.getMappedPort(REDIS_PORT);
+
+ ctx = new SpringApplicationBuilder(SpringBootRedisSampleApplication.class)
+ .properties(
+ "--server.port=0",
+ "spring.data.redis.host=" + redisHost,
+ "spring.data.redis.port=" + redisPort
+ ).run();
+
+ return "http://localhost:" + getSutPort();
+ }
+
+ @Override
+ public void stopSut() {
+ if (ctx != null) { ctx.stop(); ctx.close(); }
+ if (redis.isRunning()) redis.stop();
+ }
+
+ @Override
+ public void resetStateOfSUT() {
+ ReflectionBasedRedisClient client = new ReflectionBasedRedisClient(redisHost, redisPort, 0);
+ try {
+ client.flushAll();
+ } finally {
+ client.close();
+ }
+ }
+
+ @Override
+ public ReflectionBasedRedisClient getRedisConnection() {
+ return new ReflectionBasedRedisClient(redisHost, redisPort, 0);
+ }
+
+ @Override
+ public boolean isSutRunning() {
+ return ctx != null && ctx.isRunning();
+ }
+
+ @Override
+ public String getPackagePrefixesToCover() {
+ return "com.hendisantika.";
+ }
+
+ @Override
+ public ProblemInfo getProblemInfo() {
+ return new RestProblem(
+ "http://localhost:" + getSutPort() + "/v3/api-docs", null
+ );
+ }
+
+ @Override
+ public SutInfoDto.OutputFormat getPreferredOutputFormat() {
+ return SutInfoDto.OutputFormat.JAVA_JUNIT_5;
+ }
+
+ @Override
+ public List getInfoForAuthentication() { return null; }
+
+ @Override
+ public List getDbSpecifications() { return null; }
+
+ protected int getSutPort() {
+ return (Integer)((Map) ctx.getEnvironment()
+ .getPropertySources().get("server.ports").getSource())
+ .get("local.server.port");
+ }
+}
\ No newline at end of file
diff --git a/jdk_21_maven/em/external/rest/pom.xml b/jdk_21_maven/em/external/rest/pom.xml
index 4a511f300..03934a0df 100644
--- a/jdk_21_maven/em/external/rest/pom.xml
+++ b/jdk_21_maven/em/external/rest/pom.xml
@@ -14,6 +14,7 @@
person-controller
+ spring-boot-redis-sample
\ No newline at end of file
diff --git a/jdk_21_maven/em/external/rest/spring-boot-redis-sample/pom.xml b/jdk_21_maven/em/external/rest/spring-boot-redis-sample/pom.xml
new file mode 100644
index 000000000..567189243
--- /dev/null
+++ b/jdk_21_maven/em/external/rest/spring-boot-redis-sample/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ evomaster-benchmark-jdk21-em-external-rest-spring-boot-redis-sample
+ jar
+
+
+ org.evomaster
+ evomaster-benchmark-jdk21-em-external-rest
+ 4.3.1-SNAPSHOT
+
+
+
+
+ org.testcontainers
+ testcontainers
+ compile
+
+
+ junit
+ junit
+ compile
+ 4.11
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+ spring-boot-redis-sample-evomaster-runner
+
+
+
+ em.external.id.my.hendisantika.ExternalEvoMasterController
+ org.evomaster.client.java.instrumentation.InstrumentingAgent
+ org.evomaster.client.java.instrumentation.InstrumentingAgent
+ true
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jdk_21_maven/em/external/rest/spring-boot-redis-sample/src/main/java/em/external/id/my/hendisantika/springbootredissample/ExternalEvoMasterController.java b/jdk_21_maven/em/external/rest/spring-boot-redis-sample/src/main/java/em/external/id/my/hendisantika/springbootredissample/ExternalEvoMasterController.java
new file mode 100644
index 000000000..e78863838
--- /dev/null
+++ b/jdk_21_maven/em/external/rest/spring-boot-redis-sample/src/main/java/em/external/id/my/hendisantika/springbootredissample/ExternalEvoMasterController.java
@@ -0,0 +1,162 @@
+package em.external.id.my.hendisantika.springbootredissample;
+
+import org.evomaster.client.java.controller.ExternalSutController;
+import org.evomaster.client.java.controller.InstrumentedSutStarter;
+import org.evomaster.client.java.controller.api.dto.SutInfoDto;
+import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto;
+import org.evomaster.client.java.controller.problem.ProblemInfo;
+import org.evomaster.client.java.controller.problem.RestProblem;
+import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient;
+import org.evomaster.client.java.sql.DbSpecification;
+import org.testcontainers.containers.GenericContainer;
+
+import java.util.List;
+
+public class ExternalEvoMasterController extends ExternalSutController {
+
+ private static final int DEFAULT_CONTROLLER_PORT = 40100;
+ private static final int DEFAULT_SUT_PORT = 12345;
+ private static final int REDIS_PORT = 6379;
+
+ private final GenericContainer> redis = new GenericContainer<>("redis:7.0")
+ .withExposedPorts(REDIS_PORT);
+
+ private String redisHost;
+ private int redisPort;
+
+ public static void main(String[] args) {
+ int controllerPort = DEFAULT_CONTROLLER_PORT;
+ if (args.length > 0) controllerPort = Integer.parseInt(args[0]);
+
+ int sutPort = DEFAULT_SUT_PORT;
+ if (args.length > 1) sutPort = Integer.parseInt(args[1]);
+
+ String jarLocation = "cs/rest/spring-boot-redis-sample/target";
+ if (args.length > 2) jarLocation = args[2];
+ if (!jarLocation.endsWith(".jar")) {
+ jarLocation += "/spring-boot-redis-sample-sut.jar";
+ }
+
+ int timeoutSeconds = 120;
+ if (args.length > 3) timeoutSeconds = Integer.parseInt(args[3]);
+
+ String command = "java";
+ if (args.length > 4) command = args[4];
+
+ ExternalEvoMasterController controller =
+ new ExternalEvoMasterController(controllerPort, jarLocation, sutPort, timeoutSeconds, command);
+ InstrumentedSutStarter starter = new InstrumentedSutStarter(controller);
+ starter.start();
+ }
+
+ private final int sutPort;
+ private final int timeoutSeconds;
+ private String jarLocation;
+
+ public ExternalEvoMasterController() {
+ this(DEFAULT_CONTROLLER_PORT, "../target/spring-boot-redis-sample-sut.jar", DEFAULT_SUT_PORT, 120, "java");
+ }
+
+ public ExternalEvoMasterController(int controllerPort, String jarLocation, int sutPort, int timeoutSeconds, String command) {
+ this.sutPort = sutPort;
+ this.jarLocation = jarLocation;
+ this.timeoutSeconds = timeoutSeconds;
+ setControllerPort(controllerPort);
+ setJavaCommand(command);
+ }
+
+ @Override
+ public String[] getInputParameters() {
+ return new String[]{
+ "--server.port=" + sutPort,
+ "--spring.data.redis.host=" + redisHost,
+ "--spring.data.redis.port=" + redisPort
+ };
+ }
+
+ @Override
+ public String[] getJVMParameters() {
+ return new String[]{};
+ }
+
+ @Override
+ public String getBaseURL() {
+ return "http://localhost:" + sutPort;
+ }
+
+ @Override
+ public String getPathToExecutableJar() {
+ return jarLocation;
+ }
+
+ @Override
+ public String getLogMessageOfInitializedServer() {
+ return "Started SpringBootRedisSampleApplication in ";
+ }
+
+ @Override
+ public long getMaxAwaitForInitializationInSeconds() {
+ return timeoutSeconds;
+ }
+
+ @Override
+ public void preStart() {
+ redis.start();
+ redisHost = redis.getHost();
+ redisPort = redis.getMappedPort(REDIS_PORT);
+ }
+
+ @Override
+ public void postStart() {}
+
+ @Override
+ public void preStop() {}
+
+ @Override
+ public void postStop() {
+ if (redis.isRunning()) redis.stop();
+ }
+
+ @Override
+ public void resetStateOfSUT() {
+ ReflectionBasedRedisClient client = new ReflectionBasedRedisClient(redisHost, redisPort, 0);
+ try {
+ client.flushAll();
+ } finally {
+ client.close();
+ }
+ }
+
+ @Override
+ public ReflectionBasedRedisClient getRedisConnection() {
+ return new ReflectionBasedRedisClient(redisHost, redisPort, 0);
+ }
+
+ @Override
+ public String getPackagePrefixesToCover() {
+ return "id.my.hendisantika.";
+ }
+
+ @Override
+ public ProblemInfo getProblemInfo() {
+ return new RestProblem(
+ getBaseURL() + "/v3/api-docs",
+ null
+ );
+ }
+
+ @Override
+ public SutInfoDto.OutputFormat getPreferredOutputFormat() {
+ return SutInfoDto.OutputFormat.JAVA_JUNIT_5;
+ }
+
+ @Override
+ public List getInfoForAuthentication() {
+ return null;
+ }
+
+ @Override
+ public List getDbSpecifications() {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/openapi-swagger/spring-boot-redis-sample.json b/openapi-swagger/spring-boot-redis-sample.json
new file mode 100644
index 000000000..f1bb06044
--- /dev/null
+++ b/openapi-swagger/spring-boot-redis-sample.json
@@ -0,0 +1,207 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8080",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/api/users": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "getUsers",
+ "parameters": [
+ {
+ "name": "email",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "default": "yuji@yopmail.com"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/books": {
+ "get": {
+ "tags": [
+ "book-controller"
+ ],
+ "operationId": "getBooks",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 0
+ }
+ },
+ {
+ "name": "size",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 10
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/books/{isbn}": {
+ "get": {
+ "tags": [
+ "book-controller"
+ ],
+ "operationId": "getIsbn",
+ "parameters": [
+ {
+ "name": "isbn",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "$ref": "#/components/schemas/Book"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/books/categories": {
+ "get": {
+ "tags": [
+ "book-controller"
+ ],
+ "operationId": "getCategories",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Book": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "language": {
+ "type": "string"
+ },
+ "pageCount": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "price": {
+ "type": "number",
+ "format": "double"
+ },
+ "currency": {
+ "type": "string"
+ },
+ "infoLink": {
+ "type": "string"
+ },
+ "authors": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "uniqueItems": true
+ },
+ "categories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "uniqueItems": true
+ }
+ }
+ },
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/statistics/data.csv b/statistics/data.csv
index 82710e553..a9df7f958 100644
--- a/statistics/data.csv
+++ b/statistics/data.csv
@@ -47,4 +47,4 @@ TRUE,signal-registration,gRPC,Java,JDK 17,Maven,177,13652,,UNDEFINED,5,FALSE,htt
TRUE,webgoat,REST,Java,JDK 21,Maven,355,27638,H2,GPL,204,TRUE,https://github.com/WebGoat/WebGoat
FALSE,ind0,REST,Java,JDK 8,Gradle,103,17039,PostgreSQL,Proprietary,20,FALSE,UNDEFINED
FALSE,ind1,REST,Java;Kotlin,JDK 11,Gradle,163,15240,PostgreSQL,Proprietary,53,FALSE,UNDEFINED
-
+TRUE,spring-boot-redis-sample,REST,Java,JDK 21,Maven,18,868,Redis,UNDEFINED,4,FALSE,https://github.com/hendisantika/spring-boot-redis-sample
\ No newline at end of file
diff --git a/statistics/suts.R b/statistics/suts.R
index a39817b9a..f45fe484d 100644
--- a/statistics/suts.R
+++ b/statistics/suts.R
@@ -36,6 +36,7 @@ suts <- function(){
"session-service",
"spring-actuator-demo",
"spring-batch-rest",
+"spring-boot-redis-sample",
"spring-ecommerce",
"spring-rest-example",
"swagger-petstore",
diff --git a/statistics/table_emb.md b/statistics/table_emb.md
index bff09c9a9..f793f1002 100644
--- a/statistics/table_emb.md
+++ b/statistics/table_emb.md
@@ -36,6 +36,7 @@
|REST|__session-service__|1471|15|8|Java|JDK 8|Maven|MongoDB||
|REST|__spring-actuator-demo__|117|5|2|Java|JDK 8|Maven||✓|
|REST|__spring-batch-rest__|3668|65|5|Java|JDK 8|Maven|||
+|REST|__spring-boot-redis-sample__|868|18|4|Java|JDK 21|Maven|Redis||
|REST|__spring-ecommerce__|2223|58|26|Java|JDK 8|Maven|MongoOB, Redis, Elasticsearch|✓|
|REST|__spring-rest-example__|1426|32|9|Java|JDK 17|Maven|MySQL||
|REST|__swagger-petstore__|1631|23|19|Java|JDK 8|Maven|||