diff --git a/application/model/src/main/java/org/fuseri/model/dto/common/Result.java b/application/model/src/main/java/org/fuseri/model/dto/common/Result.java deleted file mode 100644 index c75e421e91732dae34d803b7e715f7c395aea240..0000000000000000000000000000000000000000 --- a/application/model/src/main/java/org/fuseri/model/dto/common/Result.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.fuseri.model.dto.common; - -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class Result<T extends DomainObjectDto> { - - private long total; - private int page; - private int pageSize; - private List<T> items; -} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java index f17cd1879f80fa2464168eaad424dd968efc56ba..d1eeba8b0e11d735fd939dea331037f6f8b5f0ee 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java @@ -8,15 +8,16 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; -import org.fuseri.model.dto.exercise.AnswersCreateDto; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import java.util.List; - /** * Represent a REST API controller for answers * Handle HTTP requests related to answers @@ -32,27 +33,6 @@ public class AnswerController { this.facade = facade; } - /** - * Create a new answer for the given question ID - * - * @param dto the AnswerCreateDto object containing information about the answer to create - * @return a ResponseEntity containing an AnswerDto object representing the newly created answer, or a 404 Not Found response - * if the question with the specified ID in dto was not found - */ - @Operation(summary = "Create new answers for question", description = "Creates new answers for question.") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Answers created successfully."), - @ApiResponse(responseCode = "400", description = "Invalid input.") - }) - @PostMapping - public ResponseEntity<List<AnswerDto>> createMultiple(@Valid @RequestBody AnswersCreateDto dto) { - try { - return ResponseEntity.status(HttpStatus.CREATED).body(facade.createMultiple(dto)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } - } - /** * Update an answer * diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java index da731cdbf9125963b480d393246dd7c93a35e22e..99022c65f0c2b0a9b5b2066cb8ca8f7e224971cd 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java @@ -3,15 +3,8 @@ package org.fuseri.moduleexercise.answer; import jakarta.transaction.Transactional; import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; -import org.fuseri.model.dto.exercise.AnswersCreateDto; -import org.fuseri.moduleexercise.question.Question; -import org.fuseri.moduleexercise.question.QuestionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; /** * Represent facade for managing answers @@ -21,58 +14,29 @@ import java.util.List; @Transactional public class AnswerFacade { private final AnswerService answerService; - private final QuestionService questionService; private final AnswerMapper mapper; /** * Constructor for AnswerFacade * - * @param answerService the service responsible for handling answer-related logic - * @param questionService the service responsible for handling question-related logic - * @param mapper the mapper responsible for converting between DTOs and entities + * @param answerService the service responsible for handling answer-related logic + * @param mapper the mapper responsible for converting between DTOs and entities */ @Autowired - public AnswerFacade(AnswerService answerService, QuestionService questionService, AnswerMapper mapper) { + public AnswerFacade(AnswerService answerService, AnswerMapper mapper) { this.answerService = answerService; - this.questionService = questionService; this.mapper = mapper; } /** - * Create a new answer for the given question ID - * - * @param dto the AnswerCreateDto object containing information about the answer to create - * @return an AnswerDto object representing the newly created answer - */ - public List<AnswerDto> createMultiple(@RequestBody AnswersCreateDto dto) { - List<Answer> createdAnswers = new ArrayList<>(); - for (var answerDto : dto.getAnswers()) { - Question question; - question = questionService.find(dto.getQuestionId()); - - Answer answer = mapper.fromCreateDto(answerDto); - answer.setQuestion(question); - var createdAnswer = answerService.create(answer); - question.getAnswers().add(answer); - createdAnswers.add(createdAnswer); - } - - return mapper.toDtoList(createdAnswers); - } - - /** - * Update an answer + * Update answer * * @param id of answer to update * @param dto dto with updated answer information */ public AnswerDto update(long id, AnswerCreateDto dto) { - var updatedAnswer = mapper.fromCreateDto(dto); - updatedAnswer.setId(id); - questionService.find(dto.getQuestionId()); - answerService.update(updatedAnswer); - return mapper.toDto(updatedAnswer); + return mapper.toDto(answerService.update(id, mapper.fromCreateDto(dto))); } /** @@ -81,15 +45,6 @@ public class AnswerFacade { * @param id of answer to delete */ public void delete(long id) { - var answer = answerService.find(id); - - Question question; - question = questionService.find(answer.getQuestion().getId()); - - var questionAnswers = question.getAnswers(); - questionAnswers.removeIf(a -> a.getId() == answer.getId()); - question.setAnswers(questionAnswers); - answerService.delete(id); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java index 3dec5ca627e8808d54e7f27593c834a73a0f5fa5..44b8d1f80ec4e4983e82bd107d308f11ba89f9b6 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java @@ -4,13 +4,29 @@ import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.moduleexercise.common.DomainMapper; +import org.fuseri.moduleexercise.question.Question; +import org.fuseri.moduleexercise.question.QuestionService; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Set; /** * Mapper between Answers and their corresponding DTOs */ -@Mapper -public interface AnswerMapper extends DomainMapper<Answer, AnswerDto> { +@Mapper(componentModel = "spring", uses = {QuestionService.class}) +public abstract class AnswerMapper implements DomainMapper<Answer, AnswerDto> { + + @Autowired + private QuestionService questionService; + + @Named("mapIdToQuestion") + public Question mapIdToQuestion(Long id) { + return questionService.find(id); + } /** * Convert DTO of type AnswerCreateDto to Answer @@ -18,12 +34,22 @@ public interface AnswerMapper extends DomainMapper<Answer, AnswerDto> { * @param dto DTO to be converted * @return corresponding Answer entity created from DTO */ - Answer fromCreateDto(AnswerCreateDto dto); + @Mapping(target = "question", source = "dto.questionId", qualifiedByName = "mapIdToQuestion") + public abstract Answer fromCreateDto(AnswerCreateDto dto); + + /** + * Convert Set of AnswerInQuestionCreateDto to List of corresponding Answers + * + * @param dtos to be converted + * @return corresponding list of Answers + */ + public abstract Set<Answer> fromCreateDtoList(List<AnswerInQuestionCreateDto> dtos); /** * Convert DTO of type AnswerInQuestionCreateDto to Answer + * * @param dto DTO to be converted * @return corresponding Answer entity created from DTO */ - Answer fromCreateDto(AnswerInQuestionCreateDto dto); + public abstract Answer fromCreateDto(AnswerInQuestionCreateDto dto); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java index 91151e34a7604b75d8ec6e7ef3a4767bbfff811a..39bf0015e1cacc60a34a78e804a77ce18ccc6875 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java @@ -1,10 +1,8 @@ package org.fuseri.moduleexercise.common; import org.fuseri.model.dto.common.DomainObjectDto; -import org.fuseri.model.dto.common.Result; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import java.util.List; @@ -42,18 +40,13 @@ public interface DomainMapper<T extends DomainObject, S extends DomainObjectDto> List<S> toDtoList(List<T> entities); /** - * Convert a {@link org.springframework.data.domain.Page} containing entities to a - * {@link Result} object containing a list of their corresponding DTO objects, along with - * pagination information + * Convert a {@link org.springframework.data.domain.Page} containing entities to a page containing DTOs * - * @param source the Page of entities to be converted - * @return a Result object containing a list of DTO objects and pagination information + * @param entities entities to be converted to DTOs + * @return a Page object containing a list of DTO objects and pagination information */ - @Mappings({ - @Mapping(target = "total", expression = "java(source.getTotalElements())"), - @Mapping(target = "page", expression = "java(source.getNumber())"), - @Mapping(target = "pageSize", expression = "java(source.getSize())"), - @Mapping(target = "items", expression = "java(toDtoList(source.getContent()))") - }) - Result<S> toResult(Page<T> source); + default Page<S> toDtoPage(Page<T> entities) { + return new PageImpl<>(toDtoList(entities.getContent()), entities.getPageable(), entities.getTotalPages()); + } + } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java index 1ce61dc3d62b559c7c6baaa1318172496529c752..ee083a04770e021365bd3565897bde39b265d7f8 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java @@ -35,13 +35,15 @@ public abstract class DomainService<T extends DomainObject> { /** * Update an entity * + * @param id the entity ID * @param entity the entity to update * @return the updated entity */ - public T update(T entity) { - if (!getRepository().existsById(entity.getId())) { + public T update(long id, T entity) { + if (!getRepository().existsById(id)) { throw new EntityNotFoundException("Entity with id " + entity.getId() + " not found."); } + entity.setId(id); return getRepository().save(entity); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java index 962d1556338a67b753497217b08ee190f578f33c..607b3814f604dcdfee01541f7a4a6d752784416f 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java @@ -7,14 +7,22 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; -import org.fuseri.model.dto.common.Result; import org.fuseri.model.dto.exercise.ExerciseCreateDto; import org.fuseri.model.dto.exercise.ExerciseDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +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.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * Represent a REST API controller for exercises @@ -78,7 +86,7 @@ public class ExerciseController { * Find exercises and return them in paginated format * * @param page the page number of the exercises to retrieve - * @return A ResponseEntity containing a Result object with a paginated list of exercises and metadata. + * @return A ResponseEntity containing paginated ExerciseDTOs. */ @Operation(summary = "Get exercises in paginated format", description = "Returns exercises in paginated format.") @ApiResponses(value = { @@ -86,27 +94,27 @@ public class ExerciseController { @ApiResponse(responseCode = "400", description = "Invalid page number supplied"), }) @GetMapping - public ResponseEntity<Result<ExerciseDto>> findAll(@PositiveOrZero @RequestParam int page) { + public ResponseEntity<Page<ExerciseDto>> findAll(@PositiveOrZero @RequestParam int page) { return ResponseEntity.ok(facade.findAll(page)); } /** * Find exercises that mach filters and return them in paginated format * - * @param page the page number of the exercises to retrieve * @param courseId the id of the course to filter by * @param difficulty the difficulty level to filter by - * @return A ResponseEntity containing a Result object with a paginated list of filtered ExerciseDto objects + * @param page the page number of the exercises to retrieve + * @return A ResponseEntity containing filtered and paginated ExerciseDTOs */ @Operation(summary = "Filter exercises per difficulty and per course", description = "Returns exercises which belong to specified course and have specified difficulty.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved filtered paginated exercises."), }) @GetMapping("filter") - public ResponseEntity<Result<ExerciseDto>> findPerDifficultyPerCourse( + public ResponseEntity<Page<ExerciseDto>> findPerDifficultyPerCourse( @PositiveOrZero @RequestParam int page, @NotNull @RequestParam long courseId, @PositiveOrZero @RequestParam int difficulty) { - Result<ExerciseDto> exercises = facade.findPerDifficultyPerCourse(page, courseId, difficulty); + Page<ExerciseDto> exercises = facade.findByCourseIdAndDifficulty(courseId, difficulty, page); return ResponseEntity.ok(exercises); } @@ -115,7 +123,7 @@ public class ExerciseController { * * @param exerciseId the ID of the exercise to find questions for * @param page the page number of the questions to retrieve - * @return a ResponseEntity containing a Result object with a list of QuestionDto objects and pagination information, + * @return a ResponseEntity containing paginated QuestionDTOs which belong to an exercise with exerciseId * or a NOT_FOUND response if the exercise ID is invalid */ @Operation(summary = "Find questions belonging to exercise by exercise ID", @@ -125,10 +133,10 @@ public class ExerciseController { @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") }) @GetMapping("/{exercise-id}/questions") - public ResponseEntity<Result<QuestionDto>> findQuestions(@NotNull @PathVariable("exercise-id") long exerciseId, - @PositiveOrZero @RequestParam int page) { + public ResponseEntity<Page<QuestionDto>> findQuestions(@NotNull @PathVariable("exercise-id") long exerciseId, + @PositiveOrZero @RequestParam int page) { try { - Result<QuestionDto> questions = facade.getQuestions(exerciseId, page); + Page<QuestionDto> questions = facade.getQuestions(exerciseId, page); return ResponseEntity.ok(questions); } catch (EntityNotFoundException e) { return ResponseEntity.notFound().build(); diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java index ef50a6d1e5d7f2d3783e9ca81b84a5fcc1f09312..35cea2f61c3bf72d6c94266cfb369eb0e0c70fcc 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java @@ -2,7 +2,6 @@ package org.fuseri.moduleexercise.exercise; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; -import org.fuseri.model.dto.common.Result; import org.fuseri.model.dto.exercise.ExerciseCreateDto; import org.fuseri.model.dto.exercise.ExerciseDto; import org.fuseri.model.dto.exercise.QuestionDto; @@ -26,8 +25,8 @@ public class ExerciseFacade { /** * Constructor for AnswerFacade * - * @param exerciseService the service responsible for handling answer-related logic - * @param exerciseMapper the mapper responsible for converting between DTOs and entities + * @param exerciseService the service responsible for handling answer-related logic + * @param exerciseMapper the mapper responsible for converting between DTOs and entities */ @Autowired public ExerciseFacade(ExerciseService exerciseService, ExerciseMapper exerciseMapper, QuestionMapper questionMapper) { @@ -64,11 +63,11 @@ public class ExerciseFacade { * Retrieve a page of Exercise entities * * @param page the page number to retrieve (0-indexed) - * @return a page of Exercise entities + * @return paginated exerciseDTOs */ @org.springframework.transaction.annotation.Transactional(readOnly = true) - public Result<ExerciseDto> findAll(int page) { - return exerciseMapper.toResult(exerciseService.findAll(page)); + public Page<ExerciseDto> findAll(int page) { + return exerciseMapper.toDtoPage(exerciseService.findAll(page)); } /** @@ -77,11 +76,11 @@ public class ExerciseFacade { * @param page the page number to retrieve * @param courseId the id of the course to filter by * @param difficulty the difficulty level to filter by - * @return a {@link Page} of {@link Exercise} objects filtered by the specified course id and difficulty level + * @return paginated exerciseDTOs filtered by the specified course ID and difficulty level */ - public Result<ExerciseDto> findPerDifficultyPerCourse(int page, long courseId, int difficulty) { - Page<Exercise> exercises = exerciseService.findPerDifficultyPerCourse(page, courseId, difficulty); - return exerciseMapper.toResult(exercises); + public Page<ExerciseDto> findByCourseIdAndDifficulty(long courseId, int difficulty, int page) { + Page<Exercise> exercises = exerciseService.findByCourseIdAndDifficulty(courseId, difficulty, page); + return exerciseMapper.toDtoPage(exercises); } /** @@ -89,12 +88,12 @@ public class ExerciseFacade { * * @param exerciseId the ID of the exercise to retrieve questions for * @param page the page number to retrieve (0-indexed) - * @return a page of Question entities associated with the specified exercise ID + * @return paginated questionDTOs associated with the specified exercise ID */ @org.springframework.transaction.annotation.Transactional(readOnly = true) - public Result<QuestionDto> getQuestions(long exerciseId, int page) { + public Page<QuestionDto> getQuestions(long exerciseId, int page) { Page<Question> questions = exerciseService.getQuestions(exerciseId, page); - return questionMapper.toResult(questions); + return questionMapper.toDtoPage(questions); } /** @@ -106,8 +105,7 @@ public class ExerciseFacade { */ public ExerciseDto update(long id, ExerciseCreateDto dto) { Exercise exercise = exerciseMapper.fromCreateDto(dto); - exercise.setId(id); - Exercise updatedExercise = exerciseService.update(exercise); + Exercise updatedExercise = exerciseService.update(id, exercise); return exerciseMapper.toDto(updatedExercise); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java index 6a4df023725e280b185915309d7e65e8ed5b9f44..02cbc047a790b6dcad94a4a88157e2c0bb850e9c 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java @@ -1,9 +1,9 @@ package org.fuseri.moduleexercise.exercise; -import org.fuseri.model.dto.common.Result; import org.fuseri.moduleexercise.question.Question; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -19,16 +19,16 @@ public interface ExerciseRepository extends JpaRepository<Exercise, Long> { /** * Filters the exercises by the specified difficulty level and course id, - * and returns a {@link Result} object containing these filtered exercises + * and returns an object containing these filtered exercises * along with pagination information * - * @param pageRequest the pagination settings for the result - * @param courseId the id of the course to filter by - * @param difficulty the difficulty level to filter by - * @return a {@link Result} object containing a list of paginated exercises that match the filter criteria + * @param courseId the id of the course to filter by + * @param difficulty the difficulty level to filter by + * @param pageable the pagination settings + * @return object containing a list of paginated exercises that match the filter criteria */ @Query("SELECT e FROM Exercise e WHERE e.courseId = :courseId AND e.difficulty = :difficulty") - Page<Exercise> filterPerDifficultyPerCourse(PageRequest pageRequest, long courseId, int difficulty); + Page<Exercise> findByCourseIdAndDifficulty(long courseId, int difficulty, Pageable pageable); @Query("SELECT q FROM Exercise e JOIN e.questions q WHERE e.id = :exerciseId") Page<Question> getQuestions(PageRequest pageRequest, @Param("exerciseId") Long exerciseId); diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java index 4aeed17d4c5e31eb71d4ab8cffd207eb1064b956..0caf8b0d14a100701416d872e7f78fa104237620 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java @@ -59,14 +59,13 @@ public class ExerciseService extends DomainService<Exercise> { /** * Retrieve a page of exercises filtered by the specified course id and difficulty level * - * @param page the page number to retrieve * @param courseId the id of the course to filter by * @param difficulty the difficulty level to filter by - * @return a {@link Page} of {@link Exercise} objects filtered by the specified course id and difficulty level + * @param page the page number to retrieve + * @return paginated exercises filtered by the specified course ID and difficulty level */ - public Page<Exercise> findPerDifficultyPerCourse(int page, long courseId, int difficulty) { - return repository.filterPerDifficultyPerCourse( - PageRequest.of(page, DEFAULT_PAGE_SIZE), courseId, difficulty); + public Page<Exercise> findByCourseIdAndDifficulty(long courseId, int difficulty, int page) { + return repository.findByCourseIdAndDifficulty(courseId, difficulty, PageRequest.of(page, DEFAULT_PAGE_SIZE)); } /** diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java index 2b44cd57aed3d954ec19b087922a6b3d3862dbba..03f62910b63062589484675c9f4d1ba436ec60c5 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java @@ -1,7 +1,16 @@ package org.fuseri.moduleexercise.question; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.common.DomainObject; import org.fuseri.moduleexercise.exercise.Exercise; @@ -27,7 +36,15 @@ public class Question extends DomainObject { private Set<Answer> answers = new HashSet<>(); @ManyToOne - @JoinColumn(name = "exercise_id", nullable=false) + @JoinColumn(name = "exercise_id", nullable = false) private Exercise exercise; + /** + * Add answers to question + * + * @param answersToAdd to add + */ + public void addAnswers(Set<Answer> answersToAdd) { + answers.addAll(answersToAdd); + } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java index 531f6e74bf71151f582b7f805e3e6e5e4d530edf..9857e1c3d46120979b04339b2ddf0293c22c0ed8 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java @@ -9,13 +9,22 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -119,7 +128,7 @@ public class QuestionController { @PutMapping("/{id}") public ResponseEntity<QuestionDto> updateQuestion(@NotNull @PathVariable long id, @Valid @RequestBody QuestionUpdateDto dto) { try { - return ResponseEntity.ok(questionFacade.update(id, dto)); + return ResponseEntity.ok(questionFacade.patchUpdate(id, dto)); } catch (EntityNotFoundException e) { return ResponseEntity.notFound().build(); } @@ -140,10 +149,20 @@ public class QuestionController { return ResponseEntity.noContent().build(); } - // @TestOnly - @DeleteMapping("/reset") - public void resetTable() { - questionFacade.resetTable(); + /** + * Adds answers to the existing question resource + * + * @param id id of question to update + * @return the LectureDto representing the updated lecture + */ + @Operation(summary = "Add answers to the existing question.") + @PatchMapping("/{id}/answers") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The question has been successfully updated"), + @ApiResponse(responseCode = "400", description = "The request body is invalid"), + @ApiResponse(responseCode = "404", description = "The question with the specified ID does not exist") + }) + public ResponseEntity<QuestionDto> addAnswers(@PathVariable Long id, @RequestBody List<AnswerInQuestionCreateDto> answerDtoList) { + return ResponseEntity.ok(questionFacade.addAnswers(id, answerDtoList)); } - } \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java index 7e99230db1d83a5d5dab0eaf4d5998ce7c44c47a..d25c06dbe6f0a05c61eae2f89fe2467aa14e6625 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java @@ -2,18 +2,15 @@ package org.fuseri.moduleexercise.question; import jakarta.transaction.Transactional; import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; -import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.answer.AnswerMapper; import org.fuseri.moduleexercise.answer.AnswerService; -import org.fuseri.moduleexercise.exercise.Exercise; -import org.fuseri.moduleexercise.exercise.ExerciseService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.HashSet; import java.util.List; /** @@ -24,18 +21,16 @@ import java.util.List; @Service public class QuestionFacade { private final QuestionService questionService; - private final ExerciseService exerciseService; private final AnswerService answerService; private final QuestionMapper questionMapper; private final AnswerMapper answerMapper; @Autowired public QuestionFacade( - QuestionService questionService, ExerciseService exerciseService, + QuestionService questionService, AnswerService answerService, QuestionMapper questionMapper, AnswerMapper answerMapper) { this.questionService = questionService; - this.exerciseService = exerciseService; this.answerService = answerService; this.questionMapper = questionMapper; this.answerMapper = answerMapper; @@ -48,8 +43,7 @@ public class QuestionFacade { * @return a QuestionDto object representing the found question */ public QuestionDto find(long id) { - var a = questionService.find(id); - return questionMapper.toDto(a); + return questionMapper.toDto(questionService.find(id)); } /** @@ -62,6 +56,17 @@ public class QuestionFacade { return answerMapper.toDtoList(answerService.findAllByQuestionId(questionId)); } + /** + * Add answers to question + * + * @param id question ID + * @param dto List with AnswerInQuestionCreateDto to add to question + * @return a QuestionDto object representing the updated question + */ + public QuestionDto addAnswers(Long id, List<AnswerInQuestionCreateDto> dto) { + return questionMapper.toDto(questionService.addAnswers(id, answerMapper.fromCreateDtoList(dto))); + } + /** * Create a new question * @@ -69,31 +74,7 @@ public class QuestionFacade { * @return a QuestionDto object representing the added question */ public QuestionDto create(QuestionCreateDto dto) { - Question question = questionMapper.fromCreateDto(dto); - - Exercise exercise; - exercise = exerciseService.find(dto.getExerciseId()); - - exercise.getQuestions().add(question); - question.setExercise(exercise); - - var answerDtos = dto.getAnswers(); - var answers = new HashSet<Answer>(); - for (var answerDto : answerDtos) { - Answer answer = answerMapper.fromCreateDto(answerDto); - answer = answerService.create(answer); - answers.add(answer); - } - - question.setAnswers(answers); - var createdQuestion = questionService.create(question); - - for (var answer : answers) { - answer.setQuestion(createdQuestion); - } - var result = questionMapper.toDto(createdQuestion); - result.setExerciseId(exercise.getId()); - return result; + return questionMapper.toDto(questionService.create(questionMapper.fromCreateDto(dto))); } /** @@ -102,17 +83,8 @@ public class QuestionFacade { * @param dto dto of updated question with correct id * @return dto of updated question */ - public QuestionDto update(long id, QuestionUpdateDto dto) { - Question question = questionMapper.fromUpdateDto(dto); - question.setId(id); - var qston = questionService.find(id); - List<Answer> questionAnswers = answerService.findAllByQuestionId(id); - question.setAnswers(new HashSet<>(questionAnswers)); - - question.setExercise(qston.getExercise()); - Question updatedQuestion = questionService.update(question); - - + public QuestionDto patchUpdate(long id, QuestionUpdateDto dto) { + var updatedQuestion = questionService.patchUpdateWithoutAnswers(id, questionMapper.fromUpdateDto(dto)); return questionMapper.toDto(updatedQuestion); } @@ -122,10 +94,6 @@ public class QuestionFacade { * @param id of qustion to delete */ public void delete(long id) { - var question = questionService.find(id); - for (var answer : question.getAnswers()) { - answerService.delete(answer.getId()); - } questionService.delete(id); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java index 9c4e62a2a7845c32941b219a539030fcdbf98a16..b3cd97079670ef1e2c239e08076a8748643b7110 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java @@ -1,17 +1,53 @@ package org.fuseri.moduleexercise.question; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; +import org.fuseri.moduleexercise.answer.Answer; +import org.fuseri.moduleexercise.answer.AnswerMapper; import org.fuseri.moduleexercise.common.DomainMapper; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.exercise.ExerciseService; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Set; /** * Mapper between Questions and their corresponding DTOs */ -@Mapper -public interface QuestionMapper extends DomainMapper<Question, QuestionDto> { +@Mapper(componentModel = "spring", uses = {AnswerMapper.class, ExerciseService.class}) +public abstract class QuestionMapper implements DomainMapper<Question, QuestionDto> { + + @Autowired + private ExerciseService exerciseService; + + @Autowired + private AnswerMapper answerMapper; + + @Named("mapIdToExercise") + public Exercise mapIdToExercise(Long id) { + return exerciseService.find(id); + } + + @Named("mapDtoAnswersToAnswers") + public Set<Answer> mapDtoAnswersToAnswers(List<AnswerInQuestionCreateDto> dtos) { + return answerMapper.fromCreateDtoList(dtos); + } + + /** + * Convert entity to its corresponding DTO + * + * @param question to be converted + * @return corresponding DTO created from question + */ + @Override + @Mapping(target = "exerciseId", source = "question.exercise.id") + public abstract QuestionDto toDto(Question question); /** * Convert DTO of type QuestionCreateDto to Question @@ -19,8 +55,10 @@ public interface QuestionMapper extends DomainMapper<Question, QuestionDto> { * @param dto DTO to be converted * @return corresponding Question entity created from DTO */ - @Mapping(target = "answers", ignore = true) - Question fromCreateDto(QuestionCreateDto dto); + @Mapping(target = "exercise", source = "dto.exerciseId", qualifiedByName = "mapIdToExercise") + @Mapping(target = "answers", source = "dto.answers", qualifiedByName = "mapDtoAnswersToAnswers") + public abstract Question fromCreateDto(QuestionCreateDto dto); - Question fromUpdateDto(QuestionUpdateDto dto); + @Mapping(target = "exercise", source = "dto.exerciseId", qualifiedByName = "mapIdToExercise") + public abstract Question fromUpdateDto(QuestionUpdateDto dto); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java index 58171014170f952b0d42428ec6e1f1fe000d6a7e..f0676eee28256d926e768b0fee5df3cf52275fac 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java @@ -2,13 +2,15 @@ package org.fuseri.moduleexercise.question; import jakarta.persistence.EntityNotFoundException; import lombok.Getter; +import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.common.DomainService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; +import java.util.Set; + /** * Represent a service for managing Question entities */ @@ -44,10 +46,43 @@ public class QuestionService extends DomainService<Question> { .orElseThrow(() -> new EntityNotFoundException("Question '" + id + "' not found.")); } - //@TestOnly + /** + * Patch update a question. Don't update question answers. + * + * @param id the question ID + * @param updatedQuestion the question to update + * @return the updated question + */ @Transactional - public void resetId() { - repository.resetId(); + public Question patchUpdateWithoutAnswers(Long id, Question updatedQuestion) { + Optional<Question> optionalQuestion = repository.findById(id); + if (optionalQuestion.isPresent()) { + Question question = optionalQuestion.get(); + question.setText(updatedQuestion.getText()); + question.setExercise(updatedQuestion.getExercise()); + return repository.save(question); + } else { + throw new EntityNotFoundException("Question with id: " + id + " was not found."); + } } + /** + * Add answers to question with question ID. + * + * @param id of question + * @param answers to add to question + * @return updated question + */ + public Question addAnswers(Long id, Set<Answer> answers) { + Optional<Question> optionalQuestion = repository.findById(id); + if (optionalQuestion.isPresent()) { + Question question = optionalQuestion.get(); + question.addAnswers(answers); + return repository.save(question); + } else { + throw new EntityNotFoundException( + "Question with id: " + id + " was not found."); + } + + } }