diff --git a/README.md b/README.md index af2cab8..322fbc1 100644 --- a/README.md +++ b/README.md @@ -161,4 +161,5 @@ - [Spring Boot Rest Controller Unit Test with @WebMvcTest](https://www.bezkoder.com/spring-boot-webmvctest/) - [Redis Commands](https://auth0.com/blog/introduction-to-redis-install-cli-commands-and-data-types/) - [Running RedisInsight using Docker Compose](https://collabnix.com/running-redisinsight-using-docker-compose/) -- [Google Play Integrity API](https://developer.android.com/google/play/integrity) \ No newline at end of file +- [Google Play Integrity API](https://developer.android.com/google/play/integrity) +- [A Guide to Querydsl with JPA](https://www.baeldung.com/querydsl-with-jpa-tutorial) \ No newline at end of file diff --git a/pom.xml b/pom.xml index c534af9..67219d3 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,21 @@ spring-data-envers + + com.querydsl + querydsl-apt + 5.0.0 + jakarta + provided + + + + com.querydsl + querydsl-jpa + 5.0.0 + jakarta + + org.projectlombok lombok @@ -224,6 +239,23 @@ + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + com.spotify dockerfile-maven-plugin diff --git a/src/main/java/com/mb/livedataservice/api/controller/TutorialController.java b/src/main/java/com/mb/livedataservice/api/controller/TutorialController.java index f9a35ba..b740f4b 100644 --- a/src/main/java/com/mb/livedataservice/api/controller/TutorialController.java +++ b/src/main/java/com/mb/livedataservice/api/controller/TutorialController.java @@ -1,11 +1,14 @@ package com.mb.livedataservice.api.controller; +import com.mb.livedataservice.api.filter.ApiTutorialFilter; import com.mb.livedataservice.api.request.ApiTutorialRequest; import com.mb.livedataservice.api.request.ApiTutorialUpdateRequest; import com.mb.livedataservice.api.response.ApiTutorialResponse; import com.mb.livedataservice.mapper.TutorialMapper; import com.mb.livedataservice.service.TutorialService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -57,4 +60,9 @@ public ResponseEntity deleteAllTutorials() { public ResponseEntity> findByPublished() { return new ResponseEntity<>(tutorialMapper.map(tutorialService.findByPublished(true)), HttpStatus.OK); } + + @GetMapping("/tutorials/filter") + public ResponseEntity> findAll(ApiTutorialFilter apiTutorialFilter, Pageable pageable) { + return new ResponseEntity<>(tutorialService.findAll(tutorialMapper.map(apiTutorialFilter), pageable).map(tutorialMapper::map), HttpStatus.OK); + } } diff --git a/src/main/java/com/mb/livedataservice/api/filter/ApiTutorialFilter.java b/src/main/java/com/mb/livedataservice/api/filter/ApiTutorialFilter.java new file mode 100644 index 0000000..cb006cd --- /dev/null +++ b/src/main/java/com/mb/livedataservice/api/filter/ApiTutorialFilter.java @@ -0,0 +1,19 @@ +package com.mb.livedataservice.api.filter; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiTutorialFilter { + + private String title; + + private String description; + + private boolean published; +} diff --git a/src/main/java/com/mb/livedataservice/data/filter/Filter.java b/src/main/java/com/mb/livedataservice/data/filter/Filter.java new file mode 100644 index 0000000..6f038e5 --- /dev/null +++ b/src/main/java/com/mb/livedataservice/data/filter/Filter.java @@ -0,0 +1,8 @@ +package com.mb.livedataservice.data.filter; + +import com.querydsl.core.types.Predicate; + +public interface Filter { + + Predicate toPredicate(); +} diff --git a/src/main/java/com/mb/livedataservice/data/filter/TutorialFilter.java b/src/main/java/com/mb/livedataservice/data/filter/TutorialFilter.java new file mode 100644 index 0000000..cf1ad40 --- /dev/null +++ b/src/main/java/com/mb/livedataservice/data/filter/TutorialFilter.java @@ -0,0 +1,39 @@ +package com.mb.livedataservice.data.filter; + +import com.mb.livedataservice.data.model.QTutorial; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TutorialFilter implements Filter { + + private String title; + + private String description; + + private boolean published; + + @Override + public Predicate toPredicate() { + QTutorial qTutorial = QTutorial.tutorial; + BooleanExpression predicate = qTutorial.id.isNotNull(); + + if (StringUtils.isNotBlank(title)) { + predicate = predicate.and(qTutorial.title.equalsIgnoreCase(title)); + } + + if (StringUtils.isNotBlank(description)) { + predicate = predicate.and(qTutorial.description.equalsIgnoreCase(description)); + } + + return predicate; + } +} diff --git a/src/main/java/com/mb/livedataservice/data/repository/ScoreBoardRepository.java b/src/main/java/com/mb/livedataservice/data/repository/ScoreBoardRepository.java index dc37396..5167aa9 100644 --- a/src/main/java/com/mb/livedataservice/data/repository/ScoreBoardRepository.java +++ b/src/main/java/com/mb/livedataservice/data/repository/ScoreBoardRepository.java @@ -5,13 +5,14 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository -public interface ScoreBoardRepository extends JpaRepository { +public interface ScoreBoardRepository extends JpaRepository, QuerydslPredicateExecutor { Optional findByHomeTeamNameAndAwayTeamNameAndDeletedIsFalse(String homeTeamName, String awayTeamName); @@ -20,4 +21,4 @@ public interface ScoreBoardRepository extends JpaRepository { Optional findByIdAndDeletedIsFalse(Long id); List findAllByDeletedIsTrue(Sort sort); -} \ No newline at end of file +} diff --git a/src/main/java/com/mb/livedataservice/data/repository/TutorialRepository.java b/src/main/java/com/mb/livedataservice/data/repository/TutorialRepository.java index b671e84..cd1b991 100644 --- a/src/main/java/com/mb/livedataservice/data/repository/TutorialRepository.java +++ b/src/main/java/com/mb/livedataservice/data/repository/TutorialRepository.java @@ -2,10 +2,11 @@ import com.mb.livedataservice.data.model.Tutorial; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import java.util.List; -public interface TutorialRepository extends JpaRepository { +public interface TutorialRepository extends JpaRepository, QuerydslPredicateExecutor { List findByPublished(boolean published); diff --git a/src/main/java/com/mb/livedataservice/mapper/TutorialMapper.java b/src/main/java/com/mb/livedataservice/mapper/TutorialMapper.java index bcdfe92..d38435a 100644 --- a/src/main/java/com/mb/livedataservice/mapper/TutorialMapper.java +++ b/src/main/java/com/mb/livedataservice/mapper/TutorialMapper.java @@ -1,8 +1,10 @@ package com.mb.livedataservice.mapper; +import com.mb.livedataservice.api.filter.ApiTutorialFilter; import com.mb.livedataservice.api.request.ApiTutorialRequest; import com.mb.livedataservice.api.request.ApiTutorialUpdateRequest; import com.mb.livedataservice.api.response.ApiTutorialResponse; +import com.mb.livedataservice.data.filter.TutorialFilter; import com.mb.livedataservice.data.model.Tutorial; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -21,4 +23,6 @@ public interface TutorialMapper { ApiTutorialResponse map(Tutorial tutorial); List map(List tutorial); + + TutorialFilter map(ApiTutorialFilter apiTutorialFilter); } diff --git a/src/main/java/com/mb/livedataservice/service/TutorialService.java b/src/main/java/com/mb/livedataservice/service/TutorialService.java index b2f7524..547962b 100644 --- a/src/main/java/com/mb/livedataservice/service/TutorialService.java +++ b/src/main/java/com/mb/livedataservice/service/TutorialService.java @@ -1,6 +1,9 @@ package com.mb.livedataservice.service; +import com.mb.livedataservice.data.filter.TutorialFilter; import com.mb.livedataservice.data.model.Tutorial; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import java.util.List; @@ -19,4 +22,6 @@ public interface TutorialService { void deleteAll(); List findByPublished(boolean b); + + Page findAll(TutorialFilter filter, Pageable pageable); } diff --git a/src/main/java/com/mb/livedataservice/service/impl/TutorialServiceImpl.java b/src/main/java/com/mb/livedataservice/service/impl/TutorialServiceImpl.java index 079de3d..c095dd9 100644 --- a/src/main/java/com/mb/livedataservice/service/impl/TutorialServiceImpl.java +++ b/src/main/java/com/mb/livedataservice/service/impl/TutorialServiceImpl.java @@ -1,5 +1,6 @@ package com.mb.livedataservice.service.impl; +import com.mb.livedataservice.data.filter.TutorialFilter; import com.mb.livedataservice.data.model.Tutorial; import com.mb.livedataservice.data.repository.TutorialRepository; import com.mb.livedataservice.exception.BaseException; @@ -8,6 +9,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @@ -61,4 +64,9 @@ public void deleteAll() { public List findByPublished(boolean b) { return tutorialRepository.findByPublished(b); } -} \ No newline at end of file + + @Override + public Page findAll(TutorialFilter filter, Pageable pageable) { + return tutorialRepository.findAll(filter.toPredicate(), pageable); + } +} diff --git a/src/test/java/com/mb/livedataservice/base/BaseUnitTest.java b/src/test/java/com/mb/livedataservice/base/BaseUnitTest.java index f9b7fc6..c2ad96d 100644 --- a/src/test/java/com/mb/livedataservice/base/BaseUnitTest.java +++ b/src/test/java/com/mb/livedataservice/base/BaseUnitTest.java @@ -1,5 +1,6 @@ package com.mb.livedataservice.base; +import com.mb.livedataservice.api.filter.ApiTutorialFilter; import com.mb.livedataservice.api.request.ApiScoreBoardRequest; import com.mb.livedataservice.api.request.ApiScoreBoardUpdateRequest; import com.mb.livedataservice.api.request.ApiTutorialRequest; @@ -139,4 +140,8 @@ public Tutorial getUpdatedTutorial() { public ApiTutorialResponse getUpdatedApiTutorialResponse() { return new ApiTutorialResponse(1, "Updated", "Updated", true); } + + public ApiTutorialFilter getApiTutorialFilter() { + return new ApiTutorialFilter("Updated", "Updated", true); + } } diff --git a/src/test/java/com/mb/livedataservice/helper/RestResponsePage.java b/src/test/java/com/mb/livedataservice/helper/RestResponsePage.java new file mode 100644 index 0000000..01f7955 --- /dev/null +++ b/src/test/java/com/mb/livedataservice/helper/RestResponsePage.java @@ -0,0 +1,42 @@ +package com.mb.livedataservice.helper; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RestResponsePage extends PageImpl { + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestResponsePage(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, + @JsonProperty("pageable") JsonNode pageable, + @JsonProperty("first") boolean first, + @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("sort") JsonNode sort, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + public RestResponsePage(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + public RestResponsePage(List content) { + super(content); + } + + public RestResponsePage() { + super(new ArrayList<>()); + } +} diff --git a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java index 6f378a8..9f1143d 100644 --- a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java +++ b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java @@ -5,6 +5,7 @@ import com.mb.livedataservice.api.response.ApiTutorialResponse; import com.mb.livedataservice.base.BaseUnitTest; import com.mb.livedataservice.data.model.Tutorial; +import com.mb.livedataservice.helper.RestResponsePage; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -12,12 +13,15 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.PostgreSQLContainer; @@ -149,4 +153,24 @@ void shouldGetAllTutorialsByPublishedTrue() { assertThat(tutorials.length).isGreaterThan(100); } -} \ No newline at end of file + + @Test + void shouldGetAllTutorialsByFilter() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + + UriComponents uriComponents = UriComponentsBuilder.fromPath("/api/tutorials/filter") + .queryParam("pageSize", "2") + .queryParam("page", "0") + .queryParam("description", "Description1") + .queryParam("published", true) + .build(); + + ResponseEntity> tutorials = restTemplate.exchange(uriComponents.toString(), HttpMethod.GET, null, responseType); + RestResponsePage tutorialsBody = tutorials.getBody(); + + assertThat(tutorials.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(tutorialsBody).isNotNull(); + assertThat(tutorialsBody.getNumberOfElements()).isGreaterThanOrEqualTo(1); + } +} diff --git a/src/test/java/com/mb/livedataservice/mapper/TutorialMapperTest.java b/src/test/java/com/mb/livedataservice/mapper/TutorialMapperTest.java index cd4c815..bddec07 100644 --- a/src/test/java/com/mb/livedataservice/mapper/TutorialMapperTest.java +++ b/src/test/java/com/mb/livedataservice/mapper/TutorialMapperTest.java @@ -1,9 +1,11 @@ package com.mb.livedataservice.mapper; +import com.mb.livedataservice.api.filter.ApiTutorialFilter; import com.mb.livedataservice.api.request.ApiTutorialRequest; import com.mb.livedataservice.api.request.ApiTutorialUpdateRequest; import com.mb.livedataservice.api.response.ApiTutorialResponse; import com.mb.livedataservice.base.BaseUnitTest; +import com.mb.livedataservice.data.filter.TutorialFilter; import com.mb.livedataservice.data.model.Tutorial; import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; @@ -73,4 +75,18 @@ void map_ListOfTutorialToListOfApiTutorialResponse_ShouldSucceed() { assertEquals(tutorials.getFirst().getDescription(), result.getFirst().getDescription()); assertEquals(tutorials.getFirst().isPublished(), result.getFirst().isPublished()); } + + @Test + void map_ApiTutorialFilterToTutorialFilter_ShouldSucceed() { + // arrange + ApiTutorialFilter apiTutorialFilter = getApiTutorialFilter(); + + // act + TutorialFilter result = tutorialMapper.map(apiTutorialFilter); + + // assertion + assertEquals(apiTutorialFilter.getTitle(), result.getTitle()); + assertEquals(apiTutorialFilter.getDescription(), result.getDescription()); + assertEquals(apiTutorialFilter.isPublished(), result.isPublished()); + } }