diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..dc404805 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM maven:3.9.9-eclipse-temurin-24 AS builder +WORKDIR /build +COPY pom.xml . +COPY src ./src +RUN mvn clean package + +FROM openjdk:24-jdk +WORKDIR /app +ARG JAR_FILE=target/springboot25-0.0.1-SNAPSHOT.jar +COPY --from=builder /build/${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/compose.yaml b/compose.yaml index 25863817..4f7d4c49 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,8 @@ services: + redis: + image: 'redis:7-alpine' + ports: + - "6379:6379" postgres: image: 'postgres:latest' container_name: 'postgres-db' @@ -9,13 +13,19 @@ services: ports: - '5432:5432' app: + build: + context: . + dockerfile: Dockerfile + image: 'catinder:latest' depends_on: + - redis - postgres - image: 'catinder:latest' env_file: - .env environment: - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 ports: - "8080:8080" diff --git a/pom.xml b/pom.xml index 3b87c695..f4674e05 100644 --- a/pom.xml +++ b/pom.xml @@ -51,12 +51,12 @@ flyway-database-postgresql - - org.springframework.boot - spring-boot-devtools - runtime - true - + + + + + + org.springframework.boot spring-boot-docker-compose @@ -148,6 +148,19 @@ com.fasterxml.jackson.core jackson-databind + + org.springframework.boot + spring-boot-starter-data-redis + 3.1.2 + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/java/org/example/springboot25/Springboot25Application.java b/src/main/java/org/example/springboot25/Springboot25Application.java index c4775056..f71cf8fd 100644 --- a/src/main/java/org/example/springboot25/Springboot25Application.java +++ b/src/main/java/org/example/springboot25/Springboot25Application.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @SpringBootApplication @EnableWebSecurity +@EnableCaching public class Springboot25Application { public static void main(String[] args) { diff --git a/src/main/java/org/example/springboot25/config/RedisCacheConfig.java b/src/main/java/org/example/springboot25/config/RedisCacheConfig.java new file mode 100644 index 00000000..20af84c1 --- /dev/null +++ b/src/main/java/org/example/springboot25/config/RedisCacheConfig.java @@ -0,0 +1,24 @@ +package org.example.springboot25.config; + +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.lettuce.LettuceConnectionFactory; + +import java.time.Duration; + +@Configuration +public class RedisCacheConfig { + + @Bean + public RedisCacheManager cacheManager(LettuceConnectionFactory cf) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) + .disableCachingNullValues(); + + return RedisCacheManager.builder(cf) + .cacheDefaults(config) + .build(); + } +} diff --git a/src/main/java/org/example/springboot25/controller/CatPhotoViewController.java b/src/main/java/org/example/springboot25/controller/CatPhotoViewController.java index 58eb9e5e..30469dd3 100644 --- a/src/main/java/org/example/springboot25/controller/CatPhotoViewController.java +++ b/src/main/java/org/example/springboot25/controller/CatPhotoViewController.java @@ -6,10 +6,11 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import java.io.Serializable; import java.util.List; @Controller // 🔹 Detta gör att Spring vet att den ska hantera HTML-sidor -public class CatPhotoViewController { +public class CatPhotoViewController implements Serializable { private final CatPhotoService catPhotoService; @@ -25,4 +26,3 @@ public String showCatPhotos(Model model) { return "catphoto/catphotos"; // 🔹 Returnerar en HTML-sida som heter "catphotos.html" } } - diff --git a/src/main/java/org/example/springboot25/controller/CatViewController.java b/src/main/java/org/example/springboot25/controller/CatViewController.java index e4d7afbf..9b1a137a 100644 --- a/src/main/java/org/example/springboot25/controller/CatViewController.java +++ b/src/main/java/org/example/springboot25/controller/CatViewController.java @@ -39,14 +39,12 @@ public CatViewController(CatService catService, UserService userService) { this.catService = catService; this.userService = userService; } - @GetMapping public String showAllCats(Model model) { List cats = catService.getAllCats(); model.addAttribute("cats", cats); return "cat/cats-list"; } - @GetMapping("/{catId}") public String showCatDetail(@PathVariable Long catId, Model model, Principal principal) { CatOutputDTO cat = catService.getCatDtoById(catId); diff --git a/src/main/java/org/example/springboot25/dto/CatOutputDTO.java b/src/main/java/org/example/springboot25/dto/CatOutputDTO.java index 502a1e6d..cb2ef0c3 100644 --- a/src/main/java/org/example/springboot25/dto/CatOutputDTO.java +++ b/src/main/java/org/example/springboot25/dto/CatOutputDTO.java @@ -1,6 +1,14 @@ package org.example.springboot25.dto; -public class CatOutputDTO { +import jakarta.persistence.CascadeType; +import jakarta.persistence.OneToMany; +import org.example.springboot25.entities.CatPhoto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class CatOutputDTO implements Serializable { private Long catId; private String catName; private String catProfilePicture; @@ -10,6 +18,17 @@ public class CatOutputDTO { private String catPersonality; private Long userId; + @OneToMany(mappedBy = "catPhotoCat", cascade = CascadeType.ALL, orphanRemoval = true) + private List catPhotos = new ArrayList<>(); + + public List getCatPhotos() { + return catPhotos; + } + + public void setCatPhotos(List catPhotos) { + this.catPhotos = catPhotos; + } + public Long getCatId() { return catId; } diff --git a/src/main/java/org/example/springboot25/entities/Cat.java b/src/main/java/org/example/springboot25/entities/Cat.java index de68d02b..6fe5cf71 100644 --- a/src/main/java/org/example/springboot25/entities/Cat.java +++ b/src/main/java/org/example/springboot25/entities/Cat.java @@ -5,12 +5,14 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import org.springframework.data.redis.core.RedisHash; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Entity -public class Cat { +public class Cat implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/example/springboot25/entities/User.java b/src/main/java/org/example/springboot25/entities/User.java index 0d79c079..2ff3d920 100644 --- a/src/main/java/org/example/springboot25/entities/User.java +++ b/src/main/java/org/example/springboot25/entities/User.java @@ -8,12 +8,13 @@ import jakarta.validation.constraints.NotNull; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Entity @Table(name = "app_user") -public class User { +public class User implements Serializable { private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(); diff --git a/src/main/java/org/example/springboot25/service/CatPhotoService.java b/src/main/java/org/example/springboot25/service/CatPhotoService.java index 6131d319..b174e49e 100644 --- a/src/main/java/org/example/springboot25/service/CatPhotoService.java +++ b/src/main/java/org/example/springboot25/service/CatPhotoService.java @@ -2,6 +2,7 @@ import org.example.springboot25.entities.CatPhoto; import org.example.springboot25.repository.CatPhotoRepository; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; @@ -50,4 +51,4 @@ public boolean deleteCatPhoto(Long id) { } return false; } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/springboot25/service/CatService.java b/src/main/java/org/example/springboot25/service/CatService.java index f1770054..5e23d43a 100644 --- a/src/main/java/org/example/springboot25/service/CatService.java +++ b/src/main/java/org/example/springboot25/service/CatService.java @@ -15,6 +15,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.WebDataBinder; @@ -93,6 +96,7 @@ public Cat updateCat(Long catId, Cat catDetails) { }).orElseThrow(() -> new NotFoundException("Cat not found with id " + catId)); } + @CacheEvict(cacheNames = "deleteCatById", key = "#catId") public void deleteCatById(Long catId) { if (!catRepository.existsById(catId)) { throw new NotFoundException("Cat not found with id " + catId); @@ -101,6 +105,7 @@ public void deleteCatById(Long catId) { catRepository.deleteById(catId); } + @CacheEvict(cacheNames = "deleteCat", key = "#catId") public void deleteCat(Long catId) { deleteCatById(catId); } @@ -109,18 +114,22 @@ public void deleteCat(Long catId) { // Externa metoder (DTO) // ======================== + @Cacheable(cacheNames = "allCats") public List getAllCats() { return catRepository.findAll().stream().map(catMapper::toDto).toList(); } + @Cacheable(cacheNames = "allCatsByUser", key = "#user") public List getAllCatsByUser(User user) { return catRepository.findAllByUserCatOwner(user).stream().map(catMapper::toDto).toList(); } + @Cacheable(cacheNames = "allCatsByName", key = "#name") public List getCatsByName(String name) { return catRepository.findByCatNameContainingIgnoreCase(name).stream().map(catMapper::toDto).toList(); } + @Cacheable(cacheNames = "catById", key = "#id") public CatOutputDTO getCatDtoById(Long id) { return catMapper.toDto(findCatById(id)); } @@ -136,6 +145,7 @@ public Cat addCat(Cat cat) { return saveCat(cat); } + @CachePut(cacheNames = "putCat", key = "#catId") public CatOutputDTO updateCat(Long catId, CatUpdateDTO dto) { Cat cat = findCatById(catId); log.info("Updating cat: {}", cat.getCatName()); @@ -143,6 +153,7 @@ public CatOutputDTO updateCat(Long catId, CatUpdateDTO dto) { return catMapper.toDto(saveCat(cat)); } + @CachePut(cacheNames = "patchCat", key = "#catId") public CatOutputDTO partialUpdateCat(Long catId, Map updates) { Cat cat = findCatById(catId); diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 0db516bc..93ca4a4d 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -9,3 +9,17 @@ spring.config.import=optional:file:.env[.properties] server.servlet.session.persistent=false catapi.key=${CATAPI_KEY} +# Enable caching +spring.cache.type=redis + +# Toggle Redis +spring.redis.enabled=true +spring.data.redis.repositories.enabled=false + +# Redis connection details +spring.data.redis.host=${SPRING_REDIS_HOST:localhost} +spring.data.redis.port=${SPRING_REDIS_PORT:6379} + +spring.datasource.url=jdbc:postgresql://postgres:5432/mydatabase +spring.datasource.username=myuser +spring.datasource.password=secret