From a32a36d05127b5703af2b3c33b691ea15230a17f Mon Sep 17 00:00:00 2001 From: Tat Uyen Tam Date: Thu, 8 Jan 2026 14:28:12 +1100 Subject: [PATCH 1/5] feat: add pagination for document, note, quiz set and attempts --- .../controller/AttemptController.java | 9 ++--- .../controller/DocumentController.java | 13 ++++---- .../controller/NoteController.java | 13 ++++---- .../controller/QuizSetController.java | 8 +++-- .../dto/response/PageResponse.java | 24 ++++++++++++++ .../repository/AttemptRepository.java | 3 ++ .../repository/DocumentRepository.java | 7 ++-- .../repository/QuizSetRepository.java | 3 ++ .../smart_notes/service/AttemptService.java | 33 ++++++++++++++++--- .../smart_notes/service/DocumentService.java | 18 ++++++++-- .../be08/smart_notes/service/NoteService.java | 23 +++++++++++++ .../smart_notes/service/QuizSetService.java | 30 +++++++++++++++-- src/main/resources/application-dev.properties | 2 +- 13 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/be08/smart_notes/dto/response/PageResponse.java diff --git a/src/main/java/com/be08/smart_notes/controller/AttemptController.java b/src/main/java/com/be08/smart_notes/controller/AttemptController.java index 5dc05f4..5de8369 100644 --- a/src/main/java/com/be08/smart_notes/controller/AttemptController.java +++ b/src/main/java/com/be08/smart_notes/controller/AttemptController.java @@ -3,6 +3,7 @@ import com.be08.smart_notes.dto.request.AttemptDetailUpdateRequest; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.AttemptResponse; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.view.AttemptView; import com.be08.smart_notes.service.AttemptService; import com.fasterxml.jackson.annotation.JsonView; @@ -15,8 +16,6 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import java.util.List; - @Controller @RequestMapping("/api/quizzes") @RequiredArgsConstructor @@ -37,8 +36,10 @@ public ResponseEntity createAttempt(@PathVariable int quizId) { @GetMapping("/{quizId}/attempts") @JsonView(AttemptView.Basic.class) - public ResponseEntity getAllAttemptsForQuiz(@PathVariable int quizId) { - List attemptResponseList = attemptService.getAllAttemptsByQuizId(quizId); + public ResponseEntity getAllAttemptsForQuiz(@PathVariable int quizId, + @RequestParam(required = false, defaultValue = "1") int page, + @RequestParam(required = false, defaultValue = "6") int size) { + PageResponse attemptResponseList = attemptService.getAllAttemptsByQuizId(quizId, page, size); ApiResponse apiResponse = ApiResponse.builder() .message("All attempts for quiz fetched successfully") .data(attemptResponseList) diff --git a/src/main/java/com/be08/smart_notes/controller/DocumentController.java b/src/main/java/com/be08/smart_notes/controller/DocumentController.java index 6e269d9..a226c62 100644 --- a/src/main/java/com/be08/smart_notes/controller/DocumentController.java +++ b/src/main/java/com/be08/smart_notes/controller/DocumentController.java @@ -2,16 +2,13 @@ import java.util.List; +import com.be08.smart_notes.dto.response.PageResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.model.Document; @@ -25,8 +22,10 @@ public class DocumentController { DocumentService documentService; @GetMapping - public ResponseEntity getAllDocuments() { - List documentList = documentService.getAllDocuments(); + public ResponseEntity getAllDocuments( + @RequestParam(required = false, defaultValue = "1") int page, + @RequestParam(required = false, defaultValue = "6") int size) { + PageResponse documentList = documentService.getAllDocuments(page, size); ApiResponse apiResponse = ApiResponse.builder() .message("All document fetched successfully") .data(documentList) diff --git a/src/main/java/com/be08/smart_notes/controller/NoteController.java b/src/main/java/com/be08/smart_notes/controller/NoteController.java index de5f08d..14b21c9 100644 --- a/src/main/java/com/be08/smart_notes/controller/NoteController.java +++ b/src/main/java/com/be08/smart_notes/controller/NoteController.java @@ -1,6 +1,7 @@ package com.be08.smart_notes.controller; import com.be08.smart_notes.dto.response.NoteResponse; +import com.be08.smart_notes.dto.response.PageResponse; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -13,8 +14,6 @@ import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.service.NoteService; -import java.util.List; - @RestController @RequestMapping("/api/documents/notes") @RequiredArgsConstructor @@ -43,11 +42,13 @@ public ResponseEntity getNote(@PathVariable int id) { } @GetMapping - public ResponseEntity getAllNotes() { - List noteList = noteService.getAllNotes(); + public ResponseEntity getAllNotes( + @RequestParam(required = false, defaultValue = "1") int page, + @RequestParam(required = false, defaultValue = "6") int size) { + PageResponse pageResponse = noteService.getAllNotes(page, size); ApiResponse apiResponse = ApiResponse.builder() - .message("Notes fetched successfully") - .data(noteList) + .message("Note fetched successfully") + .data(pageResponse) .build(); return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } diff --git a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java index eb1a205..4532025 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java @@ -2,6 +2,7 @@ import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; import com.be08.smart_notes.dto.response.ApiResponse; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizSetResponse; import com.be08.smart_notes.dto.view.QuizView; import com.be08.smart_notes.enums.OriginType; @@ -37,8 +38,11 @@ public ResponseEntity createQuizSet(@RequestBody @Valid QuizSetUpsertReq @GetMapping @JsonView(QuizView.Basic.class) - public ResponseEntity getAllQuizSets() { - List quizSetResponseList = quizSetService.getAllQuizSets(); + public ResponseEntity getAllQuizSets( + @RequestParam(required = false, defaultValue = "1") int page, + @RequestParam(required = false, defaultValue = "6") int size + ) { + PageResponse quizSetResponseList = quizSetService.getAllQuizSets(page, size); ApiResponse apiResponse = ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponseList) diff --git a/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java b/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java new file mode 100644 index 0000000..210c522 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java @@ -0,0 +1,24 @@ +package com.be08.smart_notes.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.util.Collections; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PageResponse { + int pageSize; + int currentPage; + int totalPages; + long totalElements; + + @Builder.Default + List pageData = Collections.emptyList(); +} diff --git a/src/main/java/com/be08/smart_notes/repository/AttemptRepository.java b/src/main/java/com/be08/smart_notes/repository/AttemptRepository.java index 4f91a1f..72c4214 100644 --- a/src/main/java/com/be08/smart_notes/repository/AttemptRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/AttemptRepository.java @@ -1,6 +1,8 @@ package com.be08.smart_notes.repository; import com.be08.smart_notes.model.Attempt; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -12,4 +14,5 @@ public interface AttemptRepository extends JpaRepository { Optional findByIdAndQuiz_QuizSet_UserId(int id, int userId); List findByQuizIdAndQuiz_QuizSet_UserId(int quizId, int userId); + Page findByQuizIdAndQuiz_QuizSet_UserId(int quizId, int userId, Pageable pageable); } diff --git a/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java b/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java index d4045c2..a4cb40a 100644 --- a/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.be08.smart_notes.model.Document; @@ -10,11 +12,12 @@ @Repository public interface DocumentRepository extends JpaRepository { - List findAllByUserId(Integer userId); + List findAllByUserId(int userId); + Page findAllByUserId(int userId, Pageable pageable); Optional findByIdAndUserId(int documentId, int userId); Optional findFirstByTitleAndUserId(String title, int userId); List findAllByIdIn(List ids); - List findAllByUserIdAndIdIn(Integer userId, List ids); + List findAllByUserIdAndIdIn(int userId, List ids); } diff --git a/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java b/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java index 069fc13..998c263 100644 --- a/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java @@ -2,6 +2,8 @@ import com.be08.smart_notes.enums.OriginType; import com.be08.smart_notes.model.QuizSet; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; @@ -12,5 +14,6 @@ public interface QuizSetRepository extends JpaRepository { Optional findByIdAndUserId(int quizSetId, int userId); List findAllByUserId(int userId); + Page findAllByUserId(int userId, Pageable pageable); void deleteAllByUserId(int userId); } diff --git a/src/main/java/com/be08/smart_notes/service/AttemptService.java b/src/main/java/com/be08/smart_notes/service/AttemptService.java index 2b255a0..cc97f7b 100644 --- a/src/main/java/com/be08/smart_notes/service/AttemptService.java +++ b/src/main/java/com/be08/smart_notes/service/AttemptService.java @@ -1,6 +1,7 @@ package com.be08.smart_notes.service; import com.be08.smart_notes.dto.request.AttemptDetailUpdateRequest; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.AttemptResponse; import com.be08.smart_notes.exception.AppException; import com.be08.smart_notes.exception.ErrorCode; @@ -16,6 +17,9 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -78,18 +82,39 @@ public AttemptResponse getAttemptByIdAndQuizId(int quizId, int attemptId) { } /** - * Get list of attempts by quiz id + * Get list of attempts (by pages) based on given quiz id * @param quizId id of target quiz * @return list of attempt response dto */ - public List getAllAttemptsByQuizId(int quizId) { + public PageResponse getAllAttemptsByQuizId(int quizId, int pageNumber, int pageSize) { int currentUserId = authorizationService.getCurrentUserId(); - List attempts = attemptRepository.findByQuizIdAndQuiz_QuizSet_UserId(quizId, currentUserId); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); + Page page = attemptRepository.findByQuizIdAndQuiz_QuizSet_UserId(quizId, currentUserId, pageable); + + List attempts = page.getContent().stream().map(attemptMapper::toAttemptResponse).toList(); - return attemptMapper.toAttemptResponseList(attempts); + return PageResponse.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .pageData(attempts).build(); } + /** + * Get list of attempts by quiz id + * @param quizId id of target quiz + * @return list of attempt response dto + */ +// public List getAllAttemptsByQuizId(int quizId) { +// int currentUserId = authorizationService.getCurrentUserId(); +// +// List attempts = attemptRepository.findByQuizIdAndQuiz_QuizSet_UserId(quizId, currentUserId); +// +// return attemptMapper.toAttemptResponseList(attempts); +// } + /** * Update single attempt detail (user answer) of given attempt id and quiz id. * The method compare user answer and correct answer to set the correctness of the attempt detail. diff --git a/src/main/java/com/be08/smart_notes/service/DocumentService.java b/src/main/java/com/be08/smart_notes/service/DocumentService.java index 0c30307..f655dea 100644 --- a/src/main/java/com/be08/smart_notes/service/DocumentService.java +++ b/src/main/java/com/be08/smart_notes/service/DocumentService.java @@ -2,12 +2,16 @@ import java.util.List; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.exception.AppException; import com.be08.smart_notes.exception.ErrorCode; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import com.be08.smart_notes.model.Document; @@ -23,11 +27,21 @@ public class DocumentService { private static final String SYSTEM_SOURCE_TITLE = "__SYSTEM_UNFILED_SOURCE__"; - public List getAllDocuments() { + public PageResponse getAllDocuments(int pageNumber, int pageSize) { // Get current user int currentUserId = authorizationService.getCurrentUserId(); - return documentRepository.findAllByUserId(currentUserId); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); + Page page = documentRepository.findAllByUserId(currentUserId, pageable); + + List documents = page.stream().toList(); + + return PageResponse.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .pageData(documents).build(); } public void deleteDocument(int id) { diff --git a/src/main/java/com/be08/smart_notes/service/NoteService.java b/src/main/java/com/be08/smart_notes/service/NoteService.java index 2f1b243..a93d09b 100644 --- a/src/main/java/com/be08/smart_notes/service/NoteService.java +++ b/src/main/java/com/be08/smart_notes/service/NoteService.java @@ -4,6 +4,7 @@ import java.util.List; import com.be08.smart_notes.dto.response.NoteResponse; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.exception.AppException; import com.be08.smart_notes.exception.ErrorCode; import com.be08.smart_notes.mapper.DocumentMapper; @@ -11,6 +12,9 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import com.be08.smart_notes.dto.request.NoteUpsertRequest; @@ -56,6 +60,25 @@ public NoteResponse createNote(NoteUpsertRequest newData) { return documentMapper.toNoteResponse(savedNote); } + public PageResponse getAllNotes(int pageNumber, int pageSize) { + // Get current user id + int currentUserId = authorizationService.getCurrentUserId(); + + // Get page data + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); + Page page = documentRepository.findAllByUserId(currentUserId, pageable); + + // Get note + List noteResponses = page.getContent().stream().map(documentMapper::toNoteResponse).toList(); + + return PageResponse.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .pageData(noteResponses).build(); + } + public List getAllNotes() { // Get current user id int currentUserId = authorizationService.getCurrentUserId(); diff --git a/src/main/java/com/be08/smart_notes/service/QuizSetService.java b/src/main/java/com/be08/smart_notes/service/QuizSetService.java index 71128d4..0fe96ef 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizSetService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizSetService.java @@ -3,6 +3,7 @@ import com.be08.smart_notes.common.AppConstants; import com.be08.smart_notes.dto.QuizUpsertDTO; import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizSetResponse; import com.be08.smart_notes.enums.OriginType; import com.be08.smart_notes.exception.AppException; @@ -16,6 +17,9 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -90,13 +94,33 @@ public QuizSetResponse getQuizSetById(int quizSetId) { * Get a QuizSet with associated quizzes using given id * @return response dto for all quiz set */ - public List getAllQuizSets() { + public PageResponse getAllQuizSets(int pageNumber, int pageSize) { int currentUserId = authorizationService.getCurrentUserId(); - List quizSets = quizSetRepository.findAllByUserId(currentUserId); - return quizSetMapper.toQuizSetResponseList(quizSets); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); + Page page = quizSetRepository.findAllByUserId(currentUserId, pageable); + + List quizSetResponses = page.stream().map(quizSetMapper::toQuizSetResponse).toList(); + + return PageResponse.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .pageData(quizSetResponses).build(); } + /** + * Get a QuizSet with associated quizzes using given id + * @return response dto for all quiz set + */ +// public List getAllQuizSets() { +// int currentUserId = authorizationService.getCurrentUserId(); +// +// List quizSets = quizSetRepository.findAllByUserId(currentUserId); +// return quizSetMapper.toQuizSetResponseList(quizSets); +// } + /** * Get default QuizSet with associated quizzes * @return response dto for default quiz set diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 0f327b6..2826672 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -2,7 +2,7 @@ spring.datasource.url=jdbc:mysql://localhost:${DB_PORT}/${DB_NAME} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} -spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA Settings for Development spring.jpa.show-sql=true From 0fb901fcee0946a54cdd4ca39d432b7eb93bfe61 Mon Sep 17 00:00:00 2001 From: Tat Uyen Tam Date: Thu, 8 Jan 2026 15:10:53 +1100 Subject: [PATCH 2/5] test(note): add testing for new pagination feature --- .../smart_notes/service/AttemptService.java | 13 --- .../smart_notes/service/QuizSetService.java | 11 --- .../NoteControllerIntegrationTest.java | 4 +- .../repository/DocumentRepositoryTest.java | 88 +++++++++++++++++ .../unit/service/NoteServiceTest.java | 99 +++++++++++++++++++ 5 files changed, 189 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/be08/smart_notes/service/AttemptService.java b/src/main/java/com/be08/smart_notes/service/AttemptService.java index cc97f7b..70712c3 100644 --- a/src/main/java/com/be08/smart_notes/service/AttemptService.java +++ b/src/main/java/com/be08/smart_notes/service/AttemptService.java @@ -102,19 +102,6 @@ public PageResponse getAllAttemptsByQuizId(int quizId, int page .pageData(attempts).build(); } - /** - * Get list of attempts by quiz id - * @param quizId id of target quiz - * @return list of attempt response dto - */ -// public List getAllAttemptsByQuizId(int quizId) { -// int currentUserId = authorizationService.getCurrentUserId(); -// -// List attempts = attemptRepository.findByQuizIdAndQuiz_QuizSet_UserId(quizId, currentUserId); -// -// return attemptMapper.toAttemptResponseList(attempts); -// } - /** * Update single attempt detail (user answer) of given attempt id and quiz id. * The method compare user answer and correct answer to set the correctness of the attempt detail. diff --git a/src/main/java/com/be08/smart_notes/service/QuizSetService.java b/src/main/java/com/be08/smart_notes/service/QuizSetService.java index 0fe96ef..0d8ac92 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizSetService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizSetService.java @@ -110,17 +110,6 @@ public PageResponse getAllQuizSets(int pageNumber, int pageSize .pageData(quizSetResponses).build(); } - /** - * Get a QuizSet with associated quizzes using given id - * @return response dto for all quiz set - */ -// public List getAllQuizSets() { -// int currentUserId = authorizationService.getCurrentUserId(); -// -// List quizSets = quizSetRepository.findAllByUserId(currentUserId); -// return quizSetMapper.toQuizSetResponseList(quizSets); -// } - /** * Get default QuizSet with associated quizzes * @return response dto for default quiz set diff --git a/src/test/java/com/be08/smart_notes/integration/controller/NoteControllerIntegrationTest.java b/src/test/java/com/be08/smart_notes/integration/controller/NoteControllerIntegrationTest.java index 6810a23..4ef1b93 100644 --- a/src/test/java/com/be08/smart_notes/integration/controller/NoteControllerIntegrationTest.java +++ b/src/test/java/com/be08/smart_notes/integration/controller/NoteControllerIntegrationTest.java @@ -173,7 +173,7 @@ void shouldRetrieveAllNotesForCurrentUser() throws Exception { .with(jwtWithUserId(TEST_USER_ID)) ) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data", hasSize(1))); + .andExpect(jsonPath("$.data.pageData", hasSize(1))); } @Test @@ -187,7 +187,7 @@ void shouldReturnEmptyListWhenUserHasNoNotes() throws Exception { .with(jwtWithUserId(TEST_USER_ID)) ) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data", hasSize(0))); + .andExpect(jsonPath("$.data.pageData", hasSize(0))); } @Test diff --git a/src/test/java/com/be08/smart_notes/integration/repository/DocumentRepositoryTest.java b/src/test/java/com/be08/smart_notes/integration/repository/DocumentRepositoryTest.java index 04014cf..3246a38 100644 --- a/src/test/java/com/be08/smart_notes/integration/repository/DocumentRepositoryTest.java +++ b/src/test/java/com/be08/smart_notes/integration/repository/DocumentRepositoryTest.java @@ -6,6 +6,9 @@ import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import java.util.Arrays; import java.util.Collections; @@ -80,6 +83,91 @@ void shouldNotReturnOtherUsersDocuments() { } } + @Nested + @DisplayName("findAllByUserId(): Page") + class FindAllByUserIdPaginatedTest { + @Test + void shouldReturnFirstPageWithCorrectSize() { + // Arrange + Pageable pageable = PageRequest.of(0, 2); + + // Act + Page result = documentRepository.findAllByUserId(FIRST_USER_ID, pageable); + + // Assert + assertNotNull(result); + assertEquals(2, result.getContent().size()); + assertEquals(3, result.getTotalElements()); + assertEquals(2, result.getTotalPages()); + assertEquals(0, result.getNumber()); + assertTrue(result.getContent().stream().allMatch(doc -> doc.getUserId() == FIRST_USER_ID)); + } + + @Test + void shouldReturnSecondPageWithRemainingDocuments() { + // Arrange + Pageable pageable = PageRequest.of(1, 2); + + // Act + Page result = documentRepository.findAllByUserId(FIRST_USER_ID, pageable); + + // Assert + assertNotNull(result); + assertEquals(1, result.getContent().size()); + assertEquals(3, result.getTotalElements()); + assertEquals(2, result.getTotalPages()); + assertEquals(1, result.getNumber()); + assertTrue(result.getContent().stream().allMatch(doc -> doc.getUserId() == FIRST_USER_ID)); + } + + @Test + void shouldReturnEmptyPageWhenUserHasNoDocuments() { + // Arrange + int nonExistentUserId = 999; + Pageable pageable = PageRequest.of(0, 10); + + // Act + Page result = documentRepository.findAllByUserId(nonExistentUserId, pageable); + + // Assert + assertNotNull(result); + assertTrue(result.getContent().isEmpty()); + assertEquals(0, result.getTotalElements()); + assertEquals(0, result.getTotalPages()); + assertEquals(0, result.getNumber()); + } + + @Test + void shouldReturnEmptyPageWhenPageNumberExceedsTotalPages() { + // Arrange + Pageable pageable = PageRequest.of(10, 10); + + // Act + Page result = documentRepository.findAllByUserId(FIRST_USER_ID, pageable); + + // Assert + assertNotNull(result); + assertTrue(result.getContent().isEmpty()); + assertEquals(3, result.getTotalElements()); + assertEquals(1, result.getTotalPages()); + assertEquals(10, result.getNumber()); + } + + @Test + void shouldNotReturnOtherUsersDocuments() { + // Arrange + Pageable pageable = PageRequest.of(0, 10); + + // Act + Page result = documentRepository.findAllByUserId(FIRST_USER_ID, pageable); + + // Assert + assertNotNull(result); + assertFalse(result.getContent().stream().anyMatch(doc -> doc.getUserId() == SECOND_USER_ID)); + assertTrue(result.getContent().stream().allMatch(doc -> doc.getUserId() == FIRST_USER_ID)); + } + } + // --- findByIdAndUserId --- // @Nested @DisplayName("findByIdAndUserId(): Optional") diff --git a/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java b/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java index 532ce38..c3c3796 100644 --- a/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java +++ b/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java @@ -1,5 +1,6 @@ package com.be08.smart_notes.unit.service; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.helper.DocumentDataBuilder; import com.be08.smart_notes.dto.request.NoteUpsertRequest; import com.be08.smart_notes.dto.response.NoteResponse; @@ -20,6 +21,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import java.util.Collections; import java.util.List; @@ -176,6 +181,100 @@ void shouldReturnSingleNoteResponseListWhenGivenSingleNote() { } } + @Nested + @DisplayName("getAllNotes(): PageResponse") + class GetAllNotesPaginatedTest { + @Test + void shouldReturnEmptyPageResponseWhenNoNotesExist() { + // Arrange + int userId = existingUser.getId(); + int pageNumber = 1; + int pageSize = 10; + Pageable pageable = PageRequest.of(0, pageSize); + Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(documentRepository.findAllByUserId(userId, pageable)).thenReturn(emptyPage); + + // Act + PageResponse actualResponse = noteService.getAllNotes(pageNumber, pageSize); + + // Assert + assertNotNull(actualResponse); + assertEquals(pageNumber, actualResponse.getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageSize()); + assertEquals(0, actualResponse.getTotalPages()); + assertEquals(0, actualResponse.getTotalElements()); + assertTrue(actualResponse.getPageData().isEmpty()); + + verify(authorizationService).getCurrentUserId(); + verify(documentRepository).findAllByUserId(userId, pageable); + verify(documentMapper, never()).toNoteResponse(any()); + } + + @Test + void shouldReturnFirstPageCorrectly() { + // Arrange + int userId = existingUser.getId(); + int pageNumber = 1; + int pageSize = 10; + Pageable pageable = PageRequest.of(0, pageSize); + List noteList = List.of(existingNote, anotherExistingNote); + Page page = new PageImpl<>(noteList, pageable, 2); + + when(documentRepository.findAllByUserId(userId, pageable)).thenReturn(page); + when(documentMapper.toNoteResponse(existingNote)).thenReturn(existingNoteResponse); + when(documentMapper.toNoteResponse(anotherExistingNote)).thenReturn(anotherExistingNoteResponse); + + // Act + PageResponse actualResponse = noteService.getAllNotes(pageNumber, pageSize); + + // Assert + assertNotNull(actualResponse); + assertEquals(pageNumber, actualResponse.getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageSize()); + assertEquals(1, actualResponse.getTotalPages()); + assertEquals(2, actualResponse.getTotalElements()); + assertEquals(2, actualResponse.getPageData().size()); + assertEquals(existingNoteResponse, actualResponse.getPageData().get(0)); + assertEquals(anotherExistingNoteResponse, actualResponse.getPageData().get(1)); + + verify(authorizationService).getCurrentUserId(); + verify(documentRepository).findAllByUserId(userId, pageable); + verify(documentMapper).toNoteResponse(existingNote); + verify(documentMapper).toNoteResponse(anotherExistingNote); + } + + @Test + void shouldReturnSecondPageCorrectly() { + // Arrange + int userId = existingUser.getId(); + int pageNumber = 2; + int pageSize = 5; + Pageable pageable = PageRequest.of(1, pageSize); + List noteList = List.of(existingNote); + Page page = new PageImpl<>(noteList, pageable, 6); + + when(documentRepository.findAllByUserId(userId, pageable)).thenReturn(page); + when(documentMapper.toNoteResponse(existingNote)).thenReturn(existingNoteResponse); + + // Act + PageResponse actualResponse = noteService.getAllNotes(pageNumber, pageSize); + + // Assert + assertNotNull(actualResponse); + assertEquals(pageNumber, actualResponse.getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageSize()); + assertEquals(2, actualResponse.getTotalPages()); + assertEquals(6, actualResponse.getTotalElements()); + assertEquals(1, actualResponse.getPageData().size()); + assertEquals(existingNoteResponse, actualResponse.getPageData().get(0)); + + verify(authorizationService).getCurrentUserId(); + verify(documentRepository).findAllByUserId(userId, pageable); + verify(documentMapper).toNoteResponse(existingNote); + } + } + // --- Create note --- // @Nested @DisplayName("createNote(): NoteResponse") From 118309d3632ee25b01d6b8ad519f7b4d8f407cb6 Mon Sep 17 00:00:00 2001 From: Tat Uyen Tam Date: Fri, 9 Jan 2026 16:03:16 +1100 Subject: [PATCH 3/5] refactor: change page response DTO structure --- .../controller/DocumentController.java | 2 -- .../dto/response/PageResponse.java | 18 ++++++++++---- .../smart_notes/service/AttemptService.java | 9 +++---- .../smart_notes/service/DocumentService.java | 9 +++---- .../be08/smart_notes/service/NoteService.java | 9 +++---- .../smart_notes/service/QuizSetService.java | 9 +++---- .../unit/service/NoteServiceTest.java | 24 +++++++++---------- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/be08/smart_notes/controller/DocumentController.java b/src/main/java/com/be08/smart_notes/controller/DocumentController.java index a226c62..9491e98 100644 --- a/src/main/java/com/be08/smart_notes/controller/DocumentController.java +++ b/src/main/java/com/be08/smart_notes/controller/DocumentController.java @@ -1,7 +1,5 @@ package com.be08.smart_notes.controller; -import java.util.List; - import com.be08.smart_notes.dto.response.PageResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java b/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java index 210c522..ceaeef0 100644 --- a/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java +++ b/src/main/java/com/be08/smart_notes/dto/response/PageResponse.java @@ -14,11 +14,19 @@ @FieldDefaults(level = AccessLevel.PRIVATE) @JsonInclude(JsonInclude.Include.NON_NULL) public class PageResponse { - int pageSize; - int currentPage; - int totalPages; - long totalElements; - @Builder.Default List pageData = Collections.emptyList(); + + PageInfo pageInfo; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class PageInfo { + int pageSize; + int currentPage; + int totalPages; + long totalElements; + } } diff --git a/src/main/java/com/be08/smart_notes/service/AttemptService.java b/src/main/java/com/be08/smart_notes/service/AttemptService.java index 70712c3..9d484ad 100644 --- a/src/main/java/com/be08/smart_notes/service/AttemptService.java +++ b/src/main/java/com/be08/smart_notes/service/AttemptService.java @@ -95,10 +95,11 @@ public PageResponse getAllAttemptsByQuizId(int quizId, int page List attempts = page.getContent().stream().map(attemptMapper::toAttemptResponse).toList(); return PageResponse.builder() - .currentPage(pageNumber) - .pageSize(pageSize) - .totalPages(page.getTotalPages()) - .totalElements(page.getTotalElements()) + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) .pageData(attempts).build(); } diff --git a/src/main/java/com/be08/smart_notes/service/DocumentService.java b/src/main/java/com/be08/smart_notes/service/DocumentService.java index f655dea..118fac9 100644 --- a/src/main/java/com/be08/smart_notes/service/DocumentService.java +++ b/src/main/java/com/be08/smart_notes/service/DocumentService.java @@ -37,10 +37,11 @@ public PageResponse getAllDocuments(int pageNumber, int pageSize) { List documents = page.stream().toList(); return PageResponse.builder() - .currentPage(pageNumber) - .pageSize(pageSize) - .totalPages(page.getTotalPages()) - .totalElements(page.getTotalElements()) + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) .pageData(documents).build(); } diff --git a/src/main/java/com/be08/smart_notes/service/NoteService.java b/src/main/java/com/be08/smart_notes/service/NoteService.java index a93d09b..46e4ef6 100644 --- a/src/main/java/com/be08/smart_notes/service/NoteService.java +++ b/src/main/java/com/be08/smart_notes/service/NoteService.java @@ -72,10 +72,11 @@ public PageResponse getAllNotes(int pageNumber, int pageSize) { List noteResponses = page.getContent().stream().map(documentMapper::toNoteResponse).toList(); return PageResponse.builder() - .currentPage(pageNumber) - .pageSize(pageSize) - .totalPages(page.getTotalPages()) - .totalElements(page.getTotalElements()) + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) .pageData(noteResponses).build(); } diff --git a/src/main/java/com/be08/smart_notes/service/QuizSetService.java b/src/main/java/com/be08/smart_notes/service/QuizSetService.java index 0d8ac92..ea06c32 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizSetService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizSetService.java @@ -103,10 +103,11 @@ public PageResponse getAllQuizSets(int pageNumber, int pageSize List quizSetResponses = page.stream().map(quizSetMapper::toQuizSetResponse).toList(); return PageResponse.builder() - .currentPage(pageNumber) - .pageSize(pageSize) - .totalPages(page.getTotalPages()) - .totalElements(page.getTotalElements()) + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) .pageData(quizSetResponses).build(); } diff --git a/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java b/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java index c3c3796..7f6a4f4 100644 --- a/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java +++ b/src/test/java/com/be08/smart_notes/unit/service/NoteServiceTest.java @@ -200,10 +200,10 @@ void shouldReturnEmptyPageResponseWhenNoNotesExist() { // Assert assertNotNull(actualResponse); - assertEquals(pageNumber, actualResponse.getCurrentPage()); - assertEquals(pageSize, actualResponse.getPageSize()); - assertEquals(0, actualResponse.getTotalPages()); - assertEquals(0, actualResponse.getTotalElements()); + assertEquals(pageNumber, actualResponse.getPageInfo().getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageInfo().getPageSize()); + assertEquals(0, actualResponse.getPageInfo().getTotalPages()); + assertEquals(0, actualResponse.getPageInfo().getTotalElements()); assertTrue(actualResponse.getPageData().isEmpty()); verify(authorizationService).getCurrentUserId(); @@ -230,10 +230,10 @@ void shouldReturnFirstPageCorrectly() { // Assert assertNotNull(actualResponse); - assertEquals(pageNumber, actualResponse.getCurrentPage()); - assertEquals(pageSize, actualResponse.getPageSize()); - assertEquals(1, actualResponse.getTotalPages()); - assertEquals(2, actualResponse.getTotalElements()); + assertEquals(pageNumber, actualResponse.getPageInfo().getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageInfo().getPageSize()); + assertEquals(1, actualResponse.getPageInfo().getTotalPages()); + assertEquals(2, actualResponse.getPageInfo().getTotalElements()); assertEquals(2, actualResponse.getPageData().size()); assertEquals(existingNoteResponse, actualResponse.getPageData().get(0)); assertEquals(anotherExistingNoteResponse, actualResponse.getPageData().get(1)); @@ -262,10 +262,10 @@ void shouldReturnSecondPageCorrectly() { // Assert assertNotNull(actualResponse); - assertEquals(pageNumber, actualResponse.getCurrentPage()); - assertEquals(pageSize, actualResponse.getPageSize()); - assertEquals(2, actualResponse.getTotalPages()); - assertEquals(6, actualResponse.getTotalElements()); + assertEquals(pageNumber, actualResponse.getPageInfo().getCurrentPage()); + assertEquals(pageSize, actualResponse.getPageInfo().getPageSize()); + assertEquals(2, actualResponse.getPageInfo().getTotalPages()); + assertEquals(6, actualResponse.getPageInfo().getTotalElements()); assertEquals(1, actualResponse.getPageData().size()); assertEquals(existingNoteResponse, actualResponse.getPageData().get(0)); From d6a119187a0dcaa3e106f6a3a0e6e1ddeff71482 Mon Sep 17 00:00:00 2001 From: Tat Uyen Tam Date: Thu, 22 Jan 2026 14:29:41 +1100 Subject: [PATCH 4/5] feat(note): add specification filtering and sorting --- .../smart_notes/common/DefaultConstants.java | 11 +++++ .../controller/AttemptController.java | 5 +- .../controller/DocumentController.java | 5 +- .../controller/NoteController.java | 11 +++-- .../controller/QuizController.java | 9 +++- .../controller/QuizSetController.java | 5 +- .../smart_notes/dto/filter/NoteFilterDTO.java | 22 +++++++++ .../dto/response/NoteResponse.java | 21 ++++----- .../repository/DocumentRepository.java | 4 +- .../repository/QuizRepository.java | 3 ++ .../be08/smart_notes/service/NoteService.java | 26 +++++++++++ .../be08/smart_notes/service/QuizService.java | 20 ++++++-- .../NoteSpecificationBuilder.java | 46 +++++++++++++++++++ 13 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/be08/smart_notes/common/DefaultConstants.java create mode 100644 src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java create mode 100644 src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java diff --git a/src/main/java/com/be08/smart_notes/common/DefaultConstants.java b/src/main/java/com/be08/smart_notes/common/DefaultConstants.java new file mode 100644 index 0000000..098b1fe --- /dev/null +++ b/src/main/java/com/be08/smart_notes/common/DefaultConstants.java @@ -0,0 +1,11 @@ +package com.be08.smart_notes.common; + +public class DefaultConstants { + // Pagination + public static final String PAGE_NUMBER = "1"; + public static final String PAGE_SIZE = "6"; + + // Sorting + public static final String SORT_BY = "updatedAt"; + public static final String SORT_ORDER = "desc"; +} diff --git a/src/main/java/com/be08/smart_notes/controller/AttemptController.java b/src/main/java/com/be08/smart_notes/controller/AttemptController.java index 5de8369..3b578cc 100644 --- a/src/main/java/com/be08/smart_notes/controller/AttemptController.java +++ b/src/main/java/com/be08/smart_notes/controller/AttemptController.java @@ -1,5 +1,6 @@ package com.be08.smart_notes.controller; +import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.request.AttemptDetailUpdateRequest; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.AttemptResponse; @@ -37,8 +38,8 @@ public ResponseEntity createAttempt(@PathVariable int quizId) { @GetMapping("/{quizId}/attempts") @JsonView(AttemptView.Basic.class) public ResponseEntity getAllAttemptsForQuiz(@PathVariable int quizId, - @RequestParam(required = false, defaultValue = "1") int page, - @RequestParam(required = false, defaultValue = "6") int size) { + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse attemptResponseList = attemptService.getAllAttemptsByQuizId(quizId, page, size); ApiResponse apiResponse = ApiResponse.builder() .message("All attempts for quiz fetched successfully") diff --git a/src/main/java/com/be08/smart_notes/controller/DocumentController.java b/src/main/java/com/be08/smart_notes/controller/DocumentController.java index 9491e98..1dc6096 100644 --- a/src/main/java/com/be08/smart_notes/controller/DocumentController.java +++ b/src/main/java/com/be08/smart_notes/controller/DocumentController.java @@ -1,5 +1,6 @@ package com.be08.smart_notes.controller; +import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.response.PageResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -21,8 +22,8 @@ public class DocumentController { @GetMapping public ResponseEntity getAllDocuments( - @RequestParam(required = false, defaultValue = "1") int page, - @RequestParam(required = false, defaultValue = "6") int size) { + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse documentList = documentService.getAllDocuments(page, size); ApiResponse apiResponse = ApiResponse.builder() .message("All document fetched successfully") diff --git a/src/main/java/com/be08/smart_notes/controller/NoteController.java b/src/main/java/com/be08/smart_notes/controller/NoteController.java index 14b21c9..0207c22 100644 --- a/src/main/java/com/be08/smart_notes/controller/NoteController.java +++ b/src/main/java/com/be08/smart_notes/controller/NoteController.java @@ -1,5 +1,7 @@ package com.be08.smart_notes.controller; +import com.be08.smart_notes.common.DefaultConstants; +import com.be08.smart_notes.dto.filter.NoteFilterDTO; import com.be08.smart_notes.dto.response.NoteResponse; import com.be08.smart_notes.dto.response.PageResponse; import jakarta.validation.Valid; @@ -43,9 +45,12 @@ public ResponseEntity getNote(@PathVariable int id) { @GetMapping public ResponseEntity getAllNotes( - @RequestParam(required = false, defaultValue = "1") int page, - @RequestParam(required = false, defaultValue = "6") int size) { - PageResponse pageResponse = noteService.getAllNotes(page, size); + @ModelAttribute NoteFilterDTO filterDTO, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { + PageResponse pageResponse = noteService.getAllNotes(filterDTO, sortBy, sortOrder, page, size); ApiResponse apiResponse = ApiResponse.builder() .message("Note fetched successfully") .data(pageResponse) diff --git a/src/main/java/com/be08/smart_notes/controller/QuizController.java b/src/main/java/com/be08/smart_notes/controller/QuizController.java index f240c60..58e8821 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizController.java @@ -1,7 +1,9 @@ package com.be08.smart_notes.controller; +import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.QuizUpsertDTO; import com.be08.smart_notes.dto.response.ApiResponse; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizResponse; import com.be08.smart_notes.dto.view.QuizView; import com.be08.smart_notes.dto.view.View; @@ -39,8 +41,11 @@ public ResponseEntity createQuiz(@RequestBody @Validated(OnCreate.class) @GetMapping @JsonView(QuizView.Basic.class) - public ResponseEntity getAllQuizzes() { - List quizResponseList = quizService.getAllQuizzes(); + public ResponseEntity getAllQuizzes( + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size + ) { + PageResponse quizResponseList = quizService.getAllQuizzes(page, size); ApiResponse apiResponse = ApiResponse.builder() .message("Quizzes fetched successfully") .data(quizResponseList) diff --git a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java index 971287a..454aa2f 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java @@ -1,5 +1,6 @@ package com.be08.smart_notes.controller; +import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.PageResponse; @@ -39,8 +40,8 @@ public ResponseEntity createQuizSet(@RequestBody @Valid QuizSetUpsertReq @GetMapping @JsonView(QuizView.Basic.class) public ResponseEntity getAllQuizSets( - @RequestParam(required = false, defaultValue = "1") int page, - @RequestParam(required = false, defaultValue = "6") int size + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size ) { PageResponse quizSetResponseList = quizSetService.getAllQuizSets(page, size); ApiResponse apiResponse = ApiResponse.builder() diff --git a/src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java b/src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java new file mode 100644 index 0000000..9bb3f87 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java @@ -0,0 +1,22 @@ +package com.be08.smart_notes.dto.filter; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class NoteFilterDTO { + String keyword; + LocalDate createdFrom; + LocalDateTime createdTo; + LocalDateTime updatedFrom; + LocalDateTime updatedTo; +} diff --git a/src/main/java/com/be08/smart_notes/dto/response/NoteResponse.java b/src/main/java/com/be08/smart_notes/dto/response/NoteResponse.java index 68b8a44..d77ec41 100644 --- a/src/main/java/com/be08/smart_notes/dto/response/NoteResponse.java +++ b/src/main/java/com/be08/smart_notes/dto/response/NoteResponse.java @@ -1,10 +1,8 @@ package com.be08.smart_notes.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.experimental.FieldDefaults; import java.time.LocalDateTime; @@ -12,14 +10,11 @@ @NoArgsConstructor @AllArgsConstructor @Builder +@FieldDefaults(level = AccessLevel.PRIVATE) public class NoteResponse { - private Integer id; - private String title; - private String content; - - @JsonFormat(pattern = "dd-MM-yyyy HH:mm:ss") - private LocalDateTime createdAt; - - @JsonFormat(pattern = "dd-MM-yyyy HH:mm:ss") - private LocalDateTime updatedAt; + Integer id; + String title; + String content; + LocalDateTime createdAt; + LocalDateTime updatedAt; } diff --git a/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java b/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java index a4cb40a..99dd73d 100644 --- a/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/DocumentRepository.java @@ -5,13 +5,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import com.be08.smart_notes.model.Document; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; @Repository -public interface DocumentRepository extends JpaRepository { +public interface DocumentRepository extends JpaRepository, JpaSpecificationExecutor { List findAllByUserId(int userId); Page findAllByUserId(int userId, Pageable pageable); diff --git a/src/main/java/com/be08/smart_notes/repository/QuizRepository.java b/src/main/java/com/be08/smart_notes/repository/QuizRepository.java index 2482bdf..06798e7 100644 --- a/src/main/java/com/be08/smart_notes/repository/QuizRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/QuizRepository.java @@ -1,5 +1,7 @@ package com.be08.smart_notes.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.be08.smart_notes.model.Quiz; @@ -10,4 +12,5 @@ public interface QuizRepository extends JpaRepository { Optional findByIdAndQuizSetUserId(int id, int userId); List findAllByQuizSetUserId(int userId); + Page findAllByQuizSetUserId(int userId, Pageable pageable); } diff --git a/src/main/java/com/be08/smart_notes/service/NoteService.java b/src/main/java/com/be08/smart_notes/service/NoteService.java index 46e4ef6..420a2c8 100644 --- a/src/main/java/com/be08/smart_notes/service/NoteService.java +++ b/src/main/java/com/be08/smart_notes/service/NoteService.java @@ -3,11 +3,13 @@ import java.time.LocalDateTime; import java.util.List; +import com.be08.smart_notes.dto.filter.NoteFilterDTO; import com.be08.smart_notes.dto.response.NoteResponse; import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.exception.AppException; import com.be08.smart_notes.exception.ErrorCode; import com.be08.smart_notes.mapper.DocumentMapper; +import com.be08.smart_notes.specification.NoteSpecificationBuilder; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; @@ -15,6 +17,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import com.be08.smart_notes.dto.request.NoteUpsertRequest; @@ -60,6 +64,28 @@ public NoteResponse createNote(NoteUpsertRequest newData) { return documentMapper.toNoteResponse(savedNote); } + public PageResponse getAllNotes(NoteFilterDTO filterDTO, String sortBy, String sortOrder, int pageNumber, int pageSize) { + // Get current user id + int currentUserId = authorizationService.getCurrentUserId(); + + // Filtering and sorting + Specification spec = NoteSpecificationBuilder.getSpecification(currentUserId, filterDTO); + Sort sortOption = sortOrder.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize, sortOption); + + // Get note + Page page = documentRepository.findAll(spec, pageable); + List noteResponses = page.getContent().stream().map(documentMapper::toNoteResponse).toList(); + + return PageResponse.builder() + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) + .pageData(noteResponses).build(); + } + public PageResponse getAllNotes(int pageNumber, int pageSize) { // Get current user id int currentUserId = authorizationService.getCurrentUserId(); diff --git a/src/main/java/com/be08/smart_notes/service/QuizService.java b/src/main/java/com/be08/smart_notes/service/QuizService.java index 37a1c4f..0176300 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizService.java @@ -1,6 +1,8 @@ package com.be08.smart_notes.service; import com.be08.smart_notes.dto.QuizUpsertDTO; +import com.be08.smart_notes.dto.response.NoteResponse; +import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizResponse; import com.be08.smart_notes.exception.AppException; import com.be08.smart_notes.exception.ErrorCode; @@ -12,6 +14,9 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @@ -68,12 +73,21 @@ public QuizResponse getQuizById(int quizId) { * Get all quiz and its questions based on given id * @return quiz response list dto */ - public List getAllQuizzes() { + public PageResponse getAllQuizzes(int pageNumber, int pageSize) { int currentUserId = authorizationService.getCurrentUserId(); - List quizzes = quizRepository.findAllByQuizSetUserId(currentUserId); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); + Page page = quizRepository.findAllByQuizSetUserId(currentUserId, pageable); - return quizMapper.toQuizResponseList(quizzes); + List quizzesResponse = page.getContent().stream().map(quizMapper::toQuizResponse).toList(); + + return PageResponse.builder() + .pageInfo(PageResponse.PageInfo.builder() + .currentPage(pageNumber) + .pageSize(pageSize) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()).build()) + .pageData(quizzesResponse).build(); } /** diff --git a/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java b/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java new file mode 100644 index 0000000..465acd2 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java @@ -0,0 +1,46 @@ +package com.be08.smart_notes.specification; + +import com.be08.smart_notes.dto.filter.NoteFilterDTO; +import com.be08.smart_notes.model.Document; +import jakarta.persistence.criteria.Predicate; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.List; + +public class NoteSpecificationBuilder { + public static Specification getSpecification(int userId, NoteFilterDTO filterDTO) { + // Build Specification using root (Document), criteria query, and criteria builder + return (root, query, cb) -> { + List predicates = new ArrayList<>(); + + if (filterDTO.getKeyword() != null && !filterDTO.getKeyword().isEmpty()) { + String targetKeyword = "%" + filterDTO.getKeyword().toLowerCase() + "%"; + + Predicate titleMatch = cb.like(cb.lower(root.get("title")), targetKeyword); + Predicate contentMatch = cb.like(cb.lower(root.get("content")), targetKeyword); + + Predicate keywordPredicate = cb.or(titleMatch, contentMatch); + predicates.add(keywordPredicate); + } + + if (filterDTO.getCreatedFrom() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedFrom())); + } + + if (filterDTO.getCreatedTo() != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedTo())); + } + + if (filterDTO.getUpdatedFrom() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedFrom())); + } + if (filterDTO.getUpdatedTo() != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedTo())); + } + + predicates.add(cb.equal(root.get("userId"), userId)); + return cb.and(predicates.toArray(new Predicate[0])); + }; + } +} From 65b6c261639d890a8b4d3fb8c59665723879d6d0 Mon Sep 17 00:00:00 2001 From: Tat Uyen Tam Date: Tue, 27 Jan 2026 16:55:13 +1100 Subject: [PATCH 5/5] feat(quiz): add pagination and filtering for quiz-related features --- .../controller/NoteController.java | 4 +- .../controller/QuizController.java | 7 ++- .../controller/QuizSetController.java | 24 +++++++--- ...NoteFilterDTO.java => BasicFilterDTO.java} | 9 ++-- .../smart_notes/dto/filter/QuizFilterDTO.java | 15 +++++++ .../smart_notes/mapper/QuizSetMapper.java | 13 ++++-- .../repository/QuizRepository.java | 3 +- .../repository/QuizSetRepository.java | 3 +- .../be08/smart_notes/service/NoteService.java | 4 +- .../be08/smart_notes/service/QuizService.java | 13 ++++-- .../smart_notes/service/QuizSetService.java | 21 ++++++--- .../specification/CommonPredicateBuilder.java | 44 +++++++++++++++++++ .../NoteSpecificationBuilder.java | 30 ++----------- .../QuizSetSpecificationBuilder.java | 24 ++++++++++ .../QuizSpecificationBuilder.java | 28 ++++++++++++ 15 files changed, 183 insertions(+), 59 deletions(-) rename src/main/java/com/be08/smart_notes/dto/filter/{NoteFilterDTO.java => BasicFilterDTO.java} (70%) create mode 100644 src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java create mode 100644 src/main/java/com/be08/smart_notes/specification/CommonPredicateBuilder.java create mode 100644 src/main/java/com/be08/smart_notes/specification/QuizSetSpecificationBuilder.java create mode 100644 src/main/java/com/be08/smart_notes/specification/QuizSpecificationBuilder.java diff --git a/src/main/java/com/be08/smart_notes/controller/NoteController.java b/src/main/java/com/be08/smart_notes/controller/NoteController.java index 0207c22..7771240 100644 --- a/src/main/java/com/be08/smart_notes/controller/NoteController.java +++ b/src/main/java/com/be08/smart_notes/controller/NoteController.java @@ -1,7 +1,7 @@ package com.be08.smart_notes.controller; import com.be08.smart_notes.common.DefaultConstants; -import com.be08.smart_notes.dto.filter.NoteFilterDTO; +import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.dto.response.NoteResponse; import com.be08.smart_notes.dto.response.PageResponse; import jakarta.validation.Valid; @@ -45,7 +45,7 @@ public ResponseEntity getNote(@PathVariable int id) { @GetMapping public ResponseEntity getAllNotes( - @ModelAttribute NoteFilterDTO filterDTO, + @ModelAttribute BasicFilterDTO filterDTO, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, diff --git a/src/main/java/com/be08/smart_notes/controller/QuizController.java b/src/main/java/com/be08/smart_notes/controller/QuizController.java index 58e8821..483c6a1 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizController.java @@ -2,11 +2,11 @@ import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.QuizUpsertDTO; +import com.be08.smart_notes.dto.filter.QuizFilterDTO; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizResponse; import com.be08.smart_notes.dto.view.QuizView; -import com.be08.smart_notes.dto.view.View; import com.be08.smart_notes.service.QuizService; import com.be08.smart_notes.validation.group.OnCreate; import com.be08.smart_notes.validation.group.OnUpdate; @@ -42,10 +42,13 @@ public ResponseEntity createQuiz(@RequestBody @Validated(OnCreate.class) @GetMapping @JsonView(QuizView.Basic.class) public ResponseEntity getAllQuizzes( + @ModelAttribute QuizFilterDTO filterDTO, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size ) { - PageResponse quizResponseList = quizService.getAllQuizzes(page, size); + PageResponse quizResponseList = quizService.getAllQuizzes(filterDTO, sortBy, sortOrder, page, size); ApiResponse apiResponse = ApiResponse.builder() .message("Quizzes fetched successfully") .data(quizResponseList) diff --git a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java index 454aa2f..0b13ff0 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java @@ -1,6 +1,7 @@ package com.be08.smart_notes.controller; import com.be08.smart_notes.common.DefaultConstants; +import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.PageResponse; @@ -17,8 +18,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/quiz-sets") @RequiredArgsConstructor @@ -40,10 +39,12 @@ public ResponseEntity createQuizSet(@RequestBody @Valid QuizSetUpsertReq @GetMapping @JsonView(QuizView.Basic.class) public ResponseEntity getAllQuizSets( + @ModelAttribute BasicFilterDTO filterDTO, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, + @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, - @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size - ) { - PageResponse quizSetResponseList = quizSetService.getAllQuizSets(page, size); + @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { + PageResponse quizSetResponseList = quizSetService.getAllQuizSets(filterDTO, sortBy, sortOrder, page, size); ApiResponse apiResponse = ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponseList) @@ -65,7 +66,18 @@ public ResponseEntity getDefaultQuizSet() { @GetMapping("/{id}") @JsonView(QuizView.Detail.class) public ResponseEntity getQuizSet(@PathVariable int id) { - QuizSetResponse quizSetResponse = quizSetService.getQuizSetById(id); + QuizSetResponse quizSetResponse = quizSetService.getQuizSetById(id, false); + ApiResponse apiResponse = ApiResponse.builder() + .message("Quiz set fetched successfully") + .data(quizSetResponse) + .build(); + return ResponseEntity.status(HttpStatus.OK).body(apiResponse); + } + + @GetMapping("/{id}/quizzes") + @JsonView(QuizView.Detail.class) + public ResponseEntity getQuizSetWithQuizzes(@PathVariable int id) { + QuizSetResponse quizSetResponse = quizSetService.getQuizSetById(id, true); ApiResponse apiResponse = ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponse) diff --git a/src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java b/src/main/java/com/be08/smart_notes/dto/filter/BasicFilterDTO.java similarity index 70% rename from src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java rename to src/main/java/com/be08/smart_notes/dto/filter/BasicFilterDTO.java index 9bb3f87..06dc460 100644 --- a/src/main/java/com/be08/smart_notes/dto/filter/NoteFilterDTO.java +++ b/src/main/java/com/be08/smart_notes/dto/filter/BasicFilterDTO.java @@ -7,16 +7,15 @@ import lombok.experimental.FieldDefaults; import java.time.LocalDate; -import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class NoteFilterDTO { +public class BasicFilterDTO { String keyword; LocalDate createdFrom; - LocalDateTime createdTo; - LocalDateTime updatedFrom; - LocalDateTime updatedTo; + LocalDate createdTo; + LocalDate updatedFrom; + LocalDate updatedTo; } diff --git a/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java b/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java new file mode 100644 index 0000000..f98dc72 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java @@ -0,0 +1,15 @@ +package com.be08.smart_notes.dto.filter; + +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class QuizFilterDTO extends BasicFilterDTO{ + Integer quizSetId; +} diff --git a/src/main/java/com/be08/smart_notes/mapper/QuizSetMapper.java b/src/main/java/com/be08/smart_notes/mapper/QuizSetMapper.java index 7b484ce..89813d5 100644 --- a/src/main/java/com/be08/smart_notes/mapper/QuizSetMapper.java +++ b/src/main/java/com/be08/smart_notes/mapper/QuizSetMapper.java @@ -3,10 +3,7 @@ import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; import com.be08.smart_notes.dto.response.QuizSetResponse; import com.be08.smart_notes.model.QuizSet; -import org.mapstruct.BeanMapping; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.*; import java.util.List; @@ -18,6 +15,14 @@ public interface QuizSetMapper { void updateQuizSet(@MappingTarget QuizSet quizSet, QuizSetUpsertRequest request); // QuizSet entity <--> QuizSetResponse dto + @Named("basic") + @Mapping(target = "quizzes", ignore = true) QuizSetResponse toQuizSetResponse(QuizSet quizSet); + + @IterableMapping(qualifiedByName = "basic") List toQuizSetResponseList(List quizSet); + + // QuizSet with Quizzes + @Named("detail") + QuizSetResponse toQuizSetResponseWithQuizzes(QuizSet quizSet); } diff --git a/src/main/java/com/be08/smart_notes/repository/QuizRepository.java b/src/main/java/com/be08/smart_notes/repository/QuizRepository.java index 06798e7..d7b5411 100644 --- a/src/main/java/com/be08/smart_notes/repository/QuizRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/QuizRepository.java @@ -5,11 +5,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.be08.smart_notes.model.Quiz; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import java.util.List; import java.util.Optional; -public interface QuizRepository extends JpaRepository { +public interface QuizRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByIdAndQuizSetUserId(int id, int userId); List findAllByQuizSetUserId(int userId); Page findAllByQuizSetUserId(int userId, Pageable pageable); diff --git a/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java b/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java index 998c263..f35c16c 100644 --- a/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java +++ b/src/main/java/com/be08/smart_notes/repository/QuizSetRepository.java @@ -5,11 +5,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import java.util.List; import java.util.Optional; -public interface QuizSetRepository extends JpaRepository { +public interface QuizSetRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByUserIdAndOriginType(int userID, OriginType originType); Optional findByIdAndUserId(int quizSetId, int userId); diff --git a/src/main/java/com/be08/smart_notes/service/NoteService.java b/src/main/java/com/be08/smart_notes/service/NoteService.java index 420a2c8..b97b5de 100644 --- a/src/main/java/com/be08/smart_notes/service/NoteService.java +++ b/src/main/java/com/be08/smart_notes/service/NoteService.java @@ -3,7 +3,7 @@ import java.time.LocalDateTime; import java.util.List; -import com.be08.smart_notes.dto.filter.NoteFilterDTO; +import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.dto.response.NoteResponse; import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.exception.AppException; @@ -64,7 +64,7 @@ public NoteResponse createNote(NoteUpsertRequest newData) { return documentMapper.toNoteResponse(savedNote); } - public PageResponse getAllNotes(NoteFilterDTO filterDTO, String sortBy, String sortOrder, int pageNumber, int pageSize) { + public PageResponse getAllNotes(BasicFilterDTO filterDTO, String sortBy, String sortOrder, int pageNumber, int pageSize) { // Get current user id int currentUserId = authorizationService.getCurrentUserId(); diff --git a/src/main/java/com/be08/smart_notes/service/QuizService.java b/src/main/java/com/be08/smart_notes/service/QuizService.java index 0176300..607a9dd 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizService.java @@ -1,7 +1,7 @@ package com.be08.smart_notes.service; import com.be08.smart_notes.dto.QuizUpsertDTO; -import com.be08.smart_notes.dto.response.NoteResponse; +import com.be08.smart_notes.dto.filter.QuizFilterDTO; import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizResponse; import com.be08.smart_notes.exception.AppException; @@ -10,6 +10,7 @@ import com.be08.smart_notes.model.Quiz; import com.be08.smart_notes.model.QuizSet; import com.be08.smart_notes.repository.QuizRepository; +import com.be08.smart_notes.specification.QuizSpecificationBuilder; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; @@ -17,6 +18,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import java.util.List; @@ -73,11 +76,13 @@ public QuizResponse getQuizById(int quizId) { * Get all quiz and its questions based on given id * @return quiz response list dto */ - public PageResponse getAllQuizzes(int pageNumber, int pageSize) { + public PageResponse getAllQuizzes(QuizFilterDTO filterDTO, String sortBy, String sortOrder, int pageNumber, int pageSize) { int currentUserId = authorizationService.getCurrentUserId(); - Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); - Page page = quizRepository.findAllByQuizSetUserId(currentUserId, pageable); + Specification spec = QuizSpecificationBuilder.getSpecification(currentUserId, filterDTO); + Sort sortOption = sortOrder.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize, sortOption); + Page page = quizRepository.findAll(spec, pageable); List quizzesResponse = page.getContent().stream().map(quizMapper::toQuizResponse).toList(); diff --git a/src/main/java/com/be08/smart_notes/service/QuizSetService.java b/src/main/java/com/be08/smart_notes/service/QuizSetService.java index ea06c32..0903238 100644 --- a/src/main/java/com/be08/smart_notes/service/QuizSetService.java +++ b/src/main/java/com/be08/smart_notes/service/QuizSetService.java @@ -2,6 +2,7 @@ import com.be08.smart_notes.common.AppConstants; import com.be08.smart_notes.dto.QuizUpsertDTO; +import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.dto.request.QuizSetUpsertRequest; import com.be08.smart_notes.dto.response.PageResponse; import com.be08.smart_notes.dto.response.QuizSetResponse; @@ -13,6 +14,7 @@ import com.be08.smart_notes.model.Quiz; import com.be08.smart_notes.model.QuizSet; import com.be08.smart_notes.repository.QuizSetRepository; +import com.be08.smart_notes.specification.QuizSetSpecificationBuilder; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; @@ -20,6 +22,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -75,11 +79,12 @@ public QuizSetResponse createQuizSet(String quizSetTitle, List qu } /** - * Get a QuizSet with associated quizzes using given id + * Get a QuizSet using given id * @param quizSetId id of target quiz set + * @param includeQuizzes choices to include or not include quizzes * @return response dto for quiz set */ - public QuizSetResponse getQuizSetById(int quizSetId) { + public QuizSetResponse getQuizSetById(int quizSetId, boolean includeQuizzes) { int currentUserId = authorizationService.getCurrentUserId(); QuizSet quiz = quizSetRepository.findByIdAndUserId(quizSetId, currentUserId).orElseThrow(() -> { @@ -87,19 +92,23 @@ public QuizSetResponse getQuizSetById(int quizSetId) { return new AppException(ErrorCode.QUIZ_SET_NOT_FOUND); }); - return quizSetMapper.toQuizSetResponse(quiz); + return includeQuizzes ? quizSetMapper.toQuizSetResponseWithQuizzes(quiz) : quizSetMapper.toQuizSetResponse(quiz); } /** * Get a QuizSet with associated quizzes using given id * @return response dto for all quiz set */ - public PageResponse getAllQuizSets(int pageNumber, int pageSize) { + public PageResponse getAllQuizSets(BasicFilterDTO filterDTO, String sortBy, String sortOrder, int pageNumber, int pageSize) { int currentUserId = authorizationService.getCurrentUserId(); - Pageable pageable = PageRequest.of(pageNumber - 1, pageSize); - Page page = quizSetRepository.findAllByUserId(currentUserId, pageable); + // Filtering and Sorting + Specification spec = QuizSetSpecificationBuilder.getSpecification(currentUserId, filterDTO); + Sort sortOption = sortOrder.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); + Pageable pageable = PageRequest.of(pageNumber - 1, pageSize, sortOption); + // Get quiz set + Page page = quizSetRepository.findAll(spec, pageable); List quizSetResponses = page.stream().map(quizSetMapper::toQuizSetResponse).toList(); return PageResponse.builder() diff --git a/src/main/java/com/be08/smart_notes/specification/CommonPredicateBuilder.java b/src/main/java/com/be08/smart_notes/specification/CommonPredicateBuilder.java new file mode 100644 index 0000000..25b9fce --- /dev/null +++ b/src/main/java/com/be08/smart_notes/specification/CommonPredicateBuilder.java @@ -0,0 +1,44 @@ +package com.be08.smart_notes.specification; + +import com.be08.smart_notes.dto.filter.BasicFilterDTO; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.util.List; + +public class CommonPredicateBuilder { + public static void addDateRangePredicates(Root root, CriteriaBuilder cb, List predicates, BasicFilterDTO filterDTO) { + if (filterDTO.getCreatedFrom() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedFrom())); + } + if (filterDTO.getCreatedTo() != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedTo())); + } + + if (filterDTO.getUpdatedFrom() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedFrom())); + } + if (filterDTO.getUpdatedTo() != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedTo())); + } + } + + public static void addKeywordPredicate(Root root, CriteriaBuilder cb, List predicates, BasicFilterDTO filterDTO, String targetProp) { + if (filterDTO.getKeyword() != null && !filterDTO.getKeyword().isEmpty()) { + predicates.add(cb.like(cb.lower(root.get(targetProp)), "%" + filterDTO.getKeyword().toLowerCase() + "%")); + } + } + + public static void addKeywordOrPredicate(Root root, CriteriaBuilder cb, List predicates, BasicFilterDTO filterDTO, String targetProp1, String targetProp2) { + if (filterDTO.getKeyword() != null && !filterDTO.getKeyword().isEmpty()) { + String targetKeyword = "%" + filterDTO.getKeyword().toLowerCase() + "%"; + + Predicate firstMatch = cb.like(cb.lower(root.get(targetProp1)), targetKeyword); + Predicate secondMatch = cb.like(cb.lower(root.get(targetProp2)), targetKeyword); + + Predicate keywordPredicate = cb.or(firstMatch, secondMatch); + predicates.add(keywordPredicate); + } + } +} diff --git a/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java b/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java index 465acd2..ce90b3f 100644 --- a/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java +++ b/src/main/java/com/be08/smart_notes/specification/NoteSpecificationBuilder.java @@ -1,6 +1,6 @@ package com.be08.smart_notes.specification; -import com.be08.smart_notes.dto.filter.NoteFilterDTO; +import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.model.Document; import jakarta.persistence.criteria.Predicate; import org.springframework.data.jpa.domain.Specification; @@ -9,35 +9,13 @@ import java.util.List; public class NoteSpecificationBuilder { - public static Specification getSpecification(int userId, NoteFilterDTO filterDTO) { + public static Specification getSpecification(int userId, BasicFilterDTO filterDTO) { // Build Specification using root (Document), criteria query, and criteria builder return (root, query, cb) -> { List predicates = new ArrayList<>(); - if (filterDTO.getKeyword() != null && !filterDTO.getKeyword().isEmpty()) { - String targetKeyword = "%" + filterDTO.getKeyword().toLowerCase() + "%"; - - Predicate titleMatch = cb.like(cb.lower(root.get("title")), targetKeyword); - Predicate contentMatch = cb.like(cb.lower(root.get("content")), targetKeyword); - - Predicate keywordPredicate = cb.or(titleMatch, contentMatch); - predicates.add(keywordPredicate); - } - - if (filterDTO.getCreatedFrom() != null) { - predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedFrom())); - } - - if (filterDTO.getCreatedTo() != null) { - predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), filterDTO.getCreatedTo())); - } - - if (filterDTO.getUpdatedFrom() != null) { - predicates.add(cb.greaterThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedFrom())); - } - if (filterDTO.getUpdatedTo() != null) { - predicates.add(cb.lessThanOrEqualTo(root.get("updatedAt"), filterDTO.getUpdatedTo())); - } + CommonPredicateBuilder.addKeywordOrPredicate(root, cb, predicates, filterDTO, "title", "content"); + CommonPredicateBuilder.addDateRangePredicates(root, cb, predicates, filterDTO); predicates.add(cb.equal(root.get("userId"), userId)); return cb.and(predicates.toArray(new Predicate[0])); diff --git a/src/main/java/com/be08/smart_notes/specification/QuizSetSpecificationBuilder.java b/src/main/java/com/be08/smart_notes/specification/QuizSetSpecificationBuilder.java new file mode 100644 index 0000000..1006a0b --- /dev/null +++ b/src/main/java/com/be08/smart_notes/specification/QuizSetSpecificationBuilder.java @@ -0,0 +1,24 @@ +package com.be08.smart_notes.specification; + +import com.be08.smart_notes.dto.filter.BasicFilterDTO; +import com.be08.smart_notes.model.QuizSet; +import jakarta.persistence.criteria.Predicate; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.List; + +public class QuizSetSpecificationBuilder { + public static Specification getSpecification(int userId, BasicFilterDTO filterDTO) { + // Build Specification using root (QuizSet), criteria query, and criteria builder + return (root, query, cb) -> { + List predicates = new ArrayList<>(); + + CommonPredicateBuilder.addKeywordPredicate(root, cb, predicates, filterDTO, "title"); + CommonPredicateBuilder.addDateRangePredicates(root, cb, predicates, filterDTO); + + predicates.add(cb.equal(root.get("userId"), userId)); + return cb.and(predicates.toArray(new Predicate[0])); + }; + } +} diff --git a/src/main/java/com/be08/smart_notes/specification/QuizSpecificationBuilder.java b/src/main/java/com/be08/smart_notes/specification/QuizSpecificationBuilder.java new file mode 100644 index 0000000..5adb529 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/specification/QuizSpecificationBuilder.java @@ -0,0 +1,28 @@ +package com.be08.smart_notes.specification; + +import com.be08.smart_notes.dto.filter.QuizFilterDTO; +import com.be08.smart_notes.model.Quiz; +import jakarta.persistence.criteria.Predicate; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.List; + +public class QuizSpecificationBuilder { + public static Specification getSpecification(int userId, QuizFilterDTO filterDTO) { + // Build Specification using root (Quiz), criteria query, and criteria builder + return (root, query, cb) -> { + List predicates = new ArrayList<>(); + + CommonPredicateBuilder.addKeywordPredicate(root, cb, predicates, filterDTO, "title"); + CommonPredicateBuilder.addDateRangePredicates(root, cb, predicates, filterDTO); + + if (filterDTO.getQuizSetId() != null) { + predicates.add(cb.equal(root.get("quizSet").get("id"), filterDTO.getQuizSetId())); + } + + predicates.add(cb.equal(root.get("quizSet").get("userId"), userId)); + return cb.and(predicates.toArray(new Predicate[0])); + }; + } +}