diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..f7a845f6c9bc9970e3e8bcc4d1e2d6e2929c712a --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java @@ -0,0 +1,20 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class AnswerCreateDto { + + @NotBlank + private String text; + + @NotNull + private boolean correct; + + @NotBlank + private String questionId; +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java new file mode 100644 index 0000000000000000000000000000000000000000..433244900c287f01549b3e43a4971f68a86a2cc7 --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java @@ -0,0 +1,38 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.fuseri.model.dto.common.DomainObjectDto; + +import java.util.Objects; + +@AllArgsConstructor +@Getter +public class AnswerDto extends DomainObjectDto { + + @NotBlank + private String text; + + @NotNull + private boolean correct; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AnswerDto answerDto = (AnswerDto) o; + + if (correct != answerDto.correct) return false; + return Objects.equals(text, answerDto.text); + } + + @Override + public int hashCode() { + int result = text != null ? text.hashCode() : 0; + result = 31 * result + (correct ? 1 : 0); + return result; + } +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..229eea30d0c9f12e27237841064dbd92489f7758 --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java @@ -0,0 +1,17 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class AnswerInQuestionCreateDto { + + @NotBlank + private String text; + + @NotNull + private boolean correct; +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..04996492928042452c248158a9d4cab9659a772a --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java @@ -0,0 +1,19 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class AnswersCreateDto { + + @NotBlank + private String questionId; + + @Valid + private List<AnswerInQuestionCreateDto> answers; +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..332b343dce447081c05db0a98e2c57df8815602e --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java @@ -0,0 +1,25 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ExerciseCreateDto { + + @NotBlank + private String name; + + @NotBlank + private String description; + + @NotNull + @PositiveOrZero + private int difficulty; + + @NotBlank + private String courseId; +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java new file mode 100644 index 0000000000000000000000000000000000000000..ff7abd444839ba89d15b50e72f31a3973b62ceef --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java @@ -0,0 +1,50 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Getter; +import lombok.Setter; +import org.fuseri.model.dto.common.DomainObjectDto; + +import java.util.Objects; + +@Getter +@Setter +public class ExerciseDto extends DomainObjectDto { + + @NotBlank + private String name; + + @NotBlank + private String description; + + @NotNull + @PositiveOrZero + private int difficulty; + + @NotBlank + private String courseId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ExerciseDto that = (ExerciseDto) o; + + if (difficulty != that.difficulty) return false; + if (!Objects.equals(name, that.name)) return false; + if (!Objects.equals(description, that.description)) return false; + return Objects.equals(courseId, that.courseId); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + difficulty; + result = 31 * result + (courseId != null ? courseId.hashCode() : 0); + return result; + } +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..738a9031739586351ba52b993c7522e0dbcb226d --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java @@ -0,0 +1,22 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class QuestionCreateDto { + + @NotBlank + private String text; + + @NotBlank + private String exerciseId; + + @Valid + private List<AnswerInQuestionCreateDto> answers; +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java new file mode 100644 index 0000000000000000000000000000000000000000..5cca20b14b60c9361eaea0893ff874500b690c61 --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java @@ -0,0 +1,44 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.fuseri.model.dto.common.DomainObjectDto; + +import java.util.List; +import java.util.Objects; + +@Getter +@Setter +public class QuestionDto extends DomainObjectDto { + + @NotBlank + private String text; + + @NotBlank + private String exerciseId; + + @Valid + private List<AnswerDto> answers; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + QuestionDto that = (QuestionDto) o; + + if (!Objects.equals(text, that.text)) return false; + if (!Objects.equals(exerciseId, that.exerciseId)) return false; + return Objects.equals(answers, that.answers); + } + + @Override + public int hashCode() { + int result = text != null ? text.hashCode() : 0; + result = 31 * result + (exerciseId != null ? exerciseId.hashCode() : 0); + result = 31 * result + (answers != null ? answers.hashCode() : 0); + return result; + } +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..2efa520493c718e108a180fabae12a4887913cab --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java @@ -0,0 +1,16 @@ +package org.fuseri.model.dto.exercise; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class QuestionUpdateDto { + + @NotBlank + private String text; + + @NotBlank + private String exerciseId; +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java new file mode 100644 index 0000000000000000000000000000000000000000..12fac75aedf87d7c1f29fe7333a24762d19a9ad3 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java @@ -0,0 +1,21 @@ +package org.fuseri.moduleexercise.answer; + +import lombok.*; +import org.fuseri.moduleexercise.common.DomainObject; + +/** + * Represent Answer entity + */ + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Answer extends DomainObject { + private String text; + + private boolean correct; + + private String questionId; +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..9f2f75a06870be2aa1bb7898a0caa471197f1681 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java @@ -0,0 +1,89 @@ +package org.fuseri.moduleexercise.answer; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +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.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +/** + * Represent a REST API controller for answers + * Handle HTTP requests related to answers + */ +@RestController +@RequestMapping("/answers") +public class AnswerController { + + private final AnswerFacade facade; + + @Autowired + public AnswerController(AnswerFacade facade) { + this.facade = facade; + } + + /** + * Retrieve a list of AnswerDto objects which belong to question with questionId + * + * @param questionId the ID of the question for which to retrieve answers + * @return a List of AnswerDto objects + */ + @GetMapping("/{question-id}") + public List<AnswerDto> findAllByQuestionId(@NotBlank @PathVariable("question-id") String questionId) { + return facade.findAllByQuestionId(questionId); + } + + /** + * 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 + * @throws ResponseStatusException if the question ID specified in the dto does not exist + */ + @PostMapping + public List<AnswerDto> createMultiple(@Valid @RequestBody AnswersCreateDto dto) { + try { + return facade.createMultiple(dto); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Update an answer + * + * @param id of answer to update + * @param dto dto with updated answer information + * @throws ResponseStatusException if the question id specified in the AnswerCreateDto dto does not exist + */ + @PutMapping("/{id}") + public AnswerDto update(@NotBlank @PathVariable String id, @Valid @RequestBody AnswerCreateDto dto) { + try { + return facade.update(id, dto); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + e.getMessage()); + } + } + + /** + * Delete answer with the given id + * + * @param id of answer to delete + * @throws ResponseStatusException if answer with specified id does not exist + */ + @DeleteMapping("/{id}") + public void delete(@NotBlank @PathVariable String id) { + try { + facade.delete(id); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..9ef573dca41c6d90e1396484a1c54e29b1730073 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java @@ -0,0 +1,112 @@ +package org.fuseri.moduleexercise.answer; + +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 + * Provide simplified interface for manipulating with answers + */ +@Service +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 + */ + @Autowired + public AnswerFacade(AnswerService answerService, QuestionService questionService, AnswerMapper mapper) { + this.answerService = answerService; + this.questionService = questionService; + this.mapper = mapper; + } + + /** + * Retrieve a list of AnswerDto objects which belong to question with questionId + * + * @param questionId the ID of the question for which to retrieve answers + * @return a List of AnswerDto objects + */ + public List<AnswerDto> findAllByQuestionId(String questionId) { + return mapper.toDtoList(answerService.findAllByQuestionId(questionId)); + } + + /** + * 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.setQuestionId(question.getId()); + var createdAnswer = answerService.create(answer); + question.getAnswers().add(answer); + createdAnswers.add(createdAnswer); + } + + return mapper.toDtoList(createdAnswers); + } + + /** + * Update an answer + * + * @param id of answer to update + * @param dto dto with updated answer information + */ + public AnswerDto update(String id, AnswerCreateDto dto) { + var updatedAnswer = mapper.fromCreateDto(dto); + updatedAnswer.setId(id); + answerService.update(updatedAnswer); + + Question question; + question = questionService.find(dto.getQuestionId()); + + var questionAnswers = question.getAnswers(); + questionAnswers.removeIf(a -> a.getId().equals(id)); + questionAnswers.add(updatedAnswer); + question.setAnswers(questionAnswers); + questionService.update(question); + + return mapper.toDto(updatedAnswer); + } + + /** + * Delete answer with the given id + * + * @param id of answer to delete + */ + public void delete(String id) { + var answer = answerService.find(id); + + Question question; + question = questionService.find(answer.getQuestionId()); + + var questionAnswers = question.getAnswers(); + questionAnswers.removeIf(a -> a.getId().equals(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 new file mode 100644 index 0000000000000000000000000000000000000000..3dec5ca627e8808d54e7f27593c834a73a0f5fa5 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java @@ -0,0 +1,29 @@ +package org.fuseri.moduleexercise.answer; + +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.mapstruct.Mapper; + +/** + * Mapper between Answers and their corresponding DTOs + */ +@Mapper +public interface AnswerMapper extends DomainMapper<Answer, AnswerDto> { + + /** + * Convert DTO of type AnswerCreateDto to Answer + * + * @param dto DTO to be converted + * @return corresponding Answer entity created from DTO + */ + Answer fromCreateDto(AnswerCreateDto dto); + + /** + * Convert DTO of type AnswerInQuestionCreateDto to Answer + * @param dto DTO to be converted + * @return corresponding Answer entity created from DTO + */ + Answer fromCreateDto(AnswerInQuestionCreateDto dto); +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..7f1918988b192f6c0f0f5fe54f92544d4d72dcbf --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java @@ -0,0 +1,20 @@ +package org.fuseri.moduleexercise.answer; + +import org.fuseri.moduleexercise.common.DomainRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * A repository interface for managing Answer entities + */ +public interface AnswerRepository extends DomainRepository<Answer, String> { + + /** + * Find all answers to a question with the specified ID + * + * @param questionId the ID of the question to find answers for + * @return a list of all answers to the specified question + */ + List<Answer> findByQuestionId(@Param("questionId") String questionId); +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..7a75a834add8e358abb04aa628ad9885a80d5705 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java @@ -0,0 +1,28 @@ +package org.fuseri.moduleexercise.answer; + +import org.fuseri.moduleexercise.common.DomainRepositoryImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * An implementation of the AnswerRepository interface + * Provides access to Answer entities stored in a data source + */ +@Repository +public class AnswerRepositoryImpl extends DomainRepositoryImpl<Answer> implements AnswerRepository { + + /** + * Find all answers to a question with the specified ID + * + * @param questionId the ID of the question to find answers for + * @return a list of all answers to the specified question + */ + @Override + public List<Answer> findByQuestionId(String questionId) { + return getItems() + .stream() + .filter(e -> e.getQuestionId().equals(questionId)) + .toList(); + } +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java new file mode 100644 index 0000000000000000000000000000000000000000..593656acfea40265f835d91d6b5f2572a8bdea31 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java @@ -0,0 +1,57 @@ +package org.fuseri.moduleexercise.answer; + +import jakarta.persistence.EntityNotFoundException; +import lombok.Getter; +import org.fuseri.moduleexercise.common.DomainService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * Represent a service for managing Answer entities + */ +@Service +public class AnswerService extends DomainService<Answer> { + + /** + * The repository instance used by this service + */ + @Getter + private final AnswerRepository repository; + + /** + * Construct a new instance of AnswerService with the specified repository + * + * @param repository the repository instance to be used by this service + */ + @Autowired + public AnswerService(AnswerRepository repository) { + this.repository = repository; + } + + /** + * Retrieve a list of Answer entities with the specified question ID + * + * @param questionId the ID of the question to retrieve answers for + * @return a list of Answer entities with the specified question ID + */ + @Transactional(readOnly = true) + public List<Answer> findAllByQuestionId(String questionId) { + return repository.findByQuestionId(questionId); + } + + /** + * Retrieve the Answer entity with the specified id + * + * @param id the id of the Answer entity to retrieve + * @return the Answer entity with the specified id + * @throws EntityNotFoundException if no Answer entity exists with the specified id + */ + @Transactional(readOnly = true) + public Answer find(String id) { + return repository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Answer '" + id + "' not found.")); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..91151e34a7604b75d8ec6e7ef3a4767bbfff811a --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java @@ -0,0 +1,59 @@ +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 java.util.List; + +/** + * Base interface for MapStruct Mappers + * It defines methods to convert DTOs to entities and vice versa + * + * @param <T> the type of the entity + * @param <S> the type of the DTO + */ +public interface DomainMapper<T extends DomainObject, S extends DomainObjectDto> { + + /** + * Convert DTO to its corresponding entity + * + * @param dto DTO to be converted + * @return corresponding entity created from DTO + */ + T fromDto(S dto); + + /** + * Convert entity to its corresponding DTO + * + * @param entity entity to be converted + * @return corresponding DTO created from entity + */ + S toDto(T entity); + + /** + * Convert List of entities to List of corresponding DTOs + * + * @param entities entities to be converted + * @return corresponding list of DTO objects + */ + 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 + * + * @param source the Page of entities to be converted + * @return a Result 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); +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java new file mode 100644 index 0000000000000000000000000000000000000000..077a8e5cdb521f4c233572accf0e6a8bb10ac4d3 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java @@ -0,0 +1,21 @@ +package org.fuseri.moduleexercise.common; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +/** + * Represent the base class for entities in the module. + */ +@Getter +@Setter +@MappedSuperclass +public abstract class DomainObject { + + @Id + private String id = UUID.randomUUID().toString(); +} + diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..fae42adfae3eabea93352b01a26b25d36418bee1 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java @@ -0,0 +1,57 @@ +package org.fuseri.moduleexercise.common; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.Optional; + +/** + * Dummy interface of repository. Later will be replaced by JpaRepository. + * + * @param <T> entity + * @param <ID> entity ID + */ +public interface DomainRepository<T, ID> { + + /** + * Save the specified entity + * + * @param entity entity to be saved + * @return created entity + */ + T save(T entity); + + /** + * Find entity by ID + * + * @param id ID of entity to be found + * @return {@code Optional} containing the found entity, + * or an empty {@code Optional} if no such entity exists + */ + Optional<T> findById(ID id); + + /** + * Retrieve a page of entities according to the specified pagination information + * + * @param pageRequest the pagination information for the query + * @return a page of entities that satisfy the pagination criteria + */ + Page<T> findAll(PageRequest pageRequest); + + /** + * Update entity + * + * @param entity entity to update + * @return updated entity + */ + T update(T entity); + + /** + * Delete the entity with the specified id + * Note that this does not do cascade deleting. + * We will have cascade deleting with usage of JpaRepository + * + * @param id the id of the entity to be deleted + */ + void deleteById(ID id); +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5c6b588fa6f6396062c93acc959d2d823284c129 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java @@ -0,0 +1,106 @@ +package org.fuseri.moduleexercise.common; + +import jakarta.persistence.EntityNotFoundException; +import lombok.Getter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Dummy implementation of repository. Later will be replaced by JpaRepository. + * + * @param <T> entity + */ +public abstract class DomainRepositoryImpl<T extends DomainObject> implements DomainRepository<T, String> { + + /** + * Dummy database + */ + @Getter + private final Set<T> items = new HashSet<>(); + + /** + * Save the specified entity + * + * @param entity entity to be saved + * @return created entity + */ + @Override + public T save(T entity) { + items.add(entity); + return entity; + } + + /** + * Find entity by ID + * + * @param id ID of entity to be found + * @return {@code Optional} containing the found entity, + * or an empty {@code Optional} if no such entity exists + */ + @Override + public Optional<T> findById(String id) { + return items.stream() + .filter(e -> e.getId().equals(id)) + .findFirst(); + } + + /** + * Retrieve a page of entities according to the specified pagination information + * + * @param pageRequest the pagination information for the query + * @return a page of entities that satisfy the pagination criteria + */ + @Override + public Page<T> findAll(PageRequest pageRequest) { + + int startIndex = pageRequest.getPageNumber() * pageRequest.getPageSize(); + + List<T> pageEntities = items.stream() + .skip(startIndex) + .limit(pageRequest.getPageSize()) + .toList(); + + return new PageImpl<>(pageEntities, pageRequest, pageEntities.size()); + } + + /** + * Update entity + * + * @param entity entity to update + * @return updated entity + */ + @Override + public T update(T entity) { + if (entity == null || entity.getId() == null) { + throw new IllegalArgumentException("Entity and its ID can not be null."); + } + + var optionalEntity = findById(entity.getId()); + if (optionalEntity.isEmpty()) { + throw new EntityNotFoundException("Entity not found with ID: " + entity.getId()); + } + + T oldEntity = optionalEntity.get(); + items.remove(oldEntity); + items.add(entity); + return entity; + } + + /** + * Delete the entity with the specified id + * Note that this does not do cascade deleting. + * We will have cascade deleting with usage of JpaRepository + * + * @param id the id of the entity to be deleted + */ + @Override + public void deleteById(String id) { + items.removeIf(e -> e.getId().equals(id)); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..badd21263def20ee0cf660c2be3f1f156a84aed4 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java @@ -0,0 +1,49 @@ +package org.fuseri.moduleexercise.common; + +/** + * Represent common service for managing entities + * + * @param <T> the type of entity managed by this service + */ +public abstract class DomainService<T extends DomainObject> { + + /** + * The default page size used for pagination + */ + public static final int DEFAULT_PAGE_SIZE = 10; + + /** + * Return the repository used by this service + * + * @return the repository used by this service + */ + public abstract DomainRepository<T, String> getRepository(); + + /** + * Create an entity by saving it to the repository + * + * @param entity the entity to create + * @return the created entity + */ + public T create(T entity) { + return getRepository().save(entity); + } + + /** + * Update an entity + * + * @param entity the entity to update + * @return the updated entity + */ + public T update(T entity) { + return getRepository().update(entity); + } + + /** + * Delete an entity with specified id + * @param id id of the entity to delete + */ + public void delete(String id) { + getRepository().deleteById(id); + } +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java new file mode 100644 index 0000000000000000000000000000000000000000..65ff0e59f9a31773168eaf378000bac45f010fd0 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java @@ -0,0 +1,49 @@ +package org.fuseri.moduleexercise.exercise; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.fuseri.moduleexercise.common.DomainObject; +import org.fuseri.moduleexercise.question.Question; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represent exercise entity + */ +@Getter +@Setter +@NoArgsConstructor +@Builder +public class Exercise extends DomainObject { + + private String name; + + private String description; + + private int difficulty; + + private String courseId; + + private Set<Question> questions = new HashSet<>(); + + /** + * Constructor of exercise + * + * @param name exercise name + * @param description exercise description + * @param difficulty exercise's difficulty + * @param courseId id of lecture to which exercise belongs + * @param questions question exercise contains + */ + public Exercise(String name, String description, int difficulty, String courseId, Set<Question> questions) { + this.name = name; + this.description = description; + this.difficulty = difficulty; + this.courseId = courseId; + this.questions = Objects.requireNonNullElseGet(questions, HashSet::new); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..1a53e6f9630c53ba29d16275f982202628181fbf --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java @@ -0,0 +1,124 @@ +package org.fuseri.moduleexercise.exercise; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +/** + * Represent a REST API controller for exercises + * Handle HTTP requests related to exercises + */ +@RestController +@RequestMapping("/exercises") +public class ExerciseController { + + private final ExerciseService service; + + private final ExerciseMapper mapper; + + /** + * Constructor for AnswerController + * + * @param service the service responsible for handling exercise-related logic + * @param mapper the mapper responsible for converting between DTOs and entities + */ + @Autowired + public ExerciseController(ExerciseService service, ExerciseMapper mapper) { + this.service = service; + this.mapper = mapper; + } + + /** + * Create a new answer for the given question ID + * + * @param dto the ExerciseCreateDto object containing information about the exercise to create + * @return an ExerciseDto object representing the newly created exercise + */ + @PostMapping + public ExerciseDto create(@Valid @RequestBody ExerciseCreateDto dto) { + Exercise exercise = mapper.fromCreateDto(dto); + return mapper.toDto(service.create(exercise)); + } + + /** + * Find an exercise by ID + * + * @param id the ID of the exercise to find + * @return an ExerciseDto object representing the found exercise + */ + @GetMapping("/{id}") + public ExerciseDto find(@NotBlank @PathVariable String id) { + return mapper.toDto(service.find(id)); + } + + /** + * Find exercises and return them in a paginated format + * + * @param page the page number of the exercises to retrieve + * @return a Result object containing a list of ExerciseDto objects and pagination information + */ + @GetMapping + public Result<ExerciseDto> findAll(@PositiveOrZero @RequestParam int page) { + Page<Exercise> exercise = service.findAll(page); + return mapper.toResult(exercise); + } + + /** + * 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 Result object containing a list of filtered ExerciseDto objects woth pagination information + */ + @GetMapping("filter") + public Result<ExerciseDto> findPerDifficultyPerCourse( + @PositiveOrZero @RequestParam int page, @NotBlank @RequestParam String courseId, + @PositiveOrZero @RequestParam int difficulty) { + Page<Exercise> exercise = service.findPerDifficultyPerCourse(page, courseId, difficulty); + return mapper.toResult(exercise); + } + + /** + * Update an exercise with id + * + * @param id the ID of the exercise to update + * @param dto the ExerciseCreateDto object containing information about the exercise to update + * @return an ExerciseDto object representing the updated exercise + * @throws ResponseStatusException invalid exercise id + */ + + @PutMapping("/{id}") + public ExerciseDto update(@NotBlank @PathVariable String id, @Valid @RequestBody ExerciseCreateDto dto) { + Exercise exercise = mapper.fromCreateDto(dto); + exercise.setId(id); + + try { + return mapper.toDto(service.update(exercise)); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Delete an exercise with exerciseId + * + * @param id the ID of the exercise to delete + */ + @DeleteMapping("/{id}") + public void delete(@NotBlank @PathVariable String id) { + service.delete(id); + } + +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..3c211aaa5d034ecba3b54e738da0ebb4315b51bf --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java @@ -0,0 +1,21 @@ +package org.fuseri.moduleexercise.exercise; + +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.fuseri.moduleexercise.common.DomainMapper; +import org.mapstruct.Mapper; + +/** + * Mapper between Exercises and their corresponding DTOs + */ +@Mapper +public interface ExerciseMapper extends DomainMapper<Exercise, ExerciseDto> { + + /** + * Convert DTO of type ExerciseCreateDto to Exercise + * + * @param dto DTO to be converted + * @return corresponding Exercise entity created from DTO + */ + Exercise fromCreateDto(ExerciseCreateDto dto); +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..76ec2a3a1eaf05eb979ccd4cdd8d5267397addb3 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java @@ -0,0 +1,24 @@ +package org.fuseri.moduleexercise.exercise; + +import org.fuseri.model.dto.common.Result; +import org.fuseri.moduleexercise.common.DomainRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +/** + * A repository interface for managing Exercise entities + */ +public interface ExerciseRepository extends DomainRepository<Exercise, String> { + + /** + * Filters the exercises by the specified difficulty level and course id, + * and returns a {@link Result} 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 + */ + Page<Exercise> filterPerDifficultyPerCourse(PageRequest pageRequest, String courseId, int difficulty); +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3036f93b02043658d38c332c45d2cbc154f308c6 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java @@ -0,0 +1,42 @@ +package org.fuseri.moduleexercise.exercise; + +import org.fuseri.model.dto.common.Result; +import org.fuseri.moduleexercise.common.DomainRepositoryImpl; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * An implementation of the ExerciseRepository interface + * Provides access to Exercise entities stored in a data source + */ +@Repository +public class ExerciseRepositoryImpl extends DomainRepositoryImpl<Exercise> implements ExerciseRepository { + + /** + * Filters the exercises by the specified difficulty level and course id, + * and returns a {@link Result} 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 + */ + @Override + public Page<Exercise> filterPerDifficultyPerCourse(PageRequest pageRequest, String courseId, int difficulty) { + + int startIndex = pageRequest.getPageNumber() * pageRequest.getPageSize(); + + List<Exercise> pageEntities = getItems().stream() + .filter(e -> e.getCourseId().equals(courseId) && e.getDifficulty() == difficulty) + .skip(startIndex) + .limit(pageRequest.getPageSize()) + .toList(); + + return new PageImpl<>(pageEntities, pageRequest, pageEntities.size()); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..677c704a7892b13672c8534c0d731d3ba7a2abb6 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java @@ -0,0 +1,70 @@ +package org.fuseri.moduleexercise.exercise; + +import jakarta.persistence.EntityNotFoundException; +import lombok.Getter; +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; + +/** + * Represent a service for managing Exercise entities + */ +@Service +public class ExerciseService extends DomainService<Exercise> { + + /** + * The repository instance used by this service + */ + @Getter + private final ExerciseRepository repository; + + /** + * Construct a new instance of ExerciseService with the specified repository + * + * @param repository the repository instance to be used by this service + */ + @Autowired + public ExerciseService(ExerciseRepository repository) { + this.repository = repository; + } + + /** + * Retrieve the Exercise entity with the specified ID + * + * @param id the ID of the Exercise entity to retrieve + * @return the Exercise entity with the specified ID + * @throws EntityNotFoundException if no Exercise entity exists with the specified ID + */ + @Transactional(readOnly = true) + public Exercise find(String id) { + return repository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Exercise '" + id + "' not found.")); + } + + /** + * Retrieve a page of Exercise entities + * + * @param page the page number to retrieve (0-indexed) + * @return a page of Exercise entities + */ + @Transactional(readOnly = true) + public Page<Exercise> findAll(int page) { + return repository.findAll(PageRequest.of(page, DEFAULT_PAGE_SIZE)); + } + + /** + * 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 + */ + public Page<Exercise> findPerDifficultyPerCourse(int page, String courseId, int difficulty) { + return repository.filterPerDifficultyPerCourse( + PageRequest.of(page, DEFAULT_PAGE_SIZE), courseId, difficulty); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..042604af6ad1e339be34b38f5481806938dfbb38 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java @@ -0,0 +1,41 @@ +package org.fuseri.moduleexercise.question; + +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 java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represent Question entity + */ +@Getter +@Setter +@NoArgsConstructor +@Builder +public class Question extends DomainObject { + + private String text; + + private Set<Answer> answers = new HashSet<>(); + + private String exerciseId; + + /** + * Constructor of question + * + * @param text question text + * @param answers question answers + * @param exerciseId id of exercise the question belongs to + */ + public Question(String text, Set<Answer> answers, String exerciseId) { + this.text = text; + this.answers = Objects.requireNonNullElseGet(answers, HashSet::new); + this.exerciseId = exerciseId; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..bd217cc1c00f36d02b1b074b6e9028c20e291220 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java @@ -0,0 +1,104 @@ +package org.fuseri.moduleexercise.question; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PositiveOrZero; +import org.fuseri.model.dto.common.Result; +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.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +/** + * Represent a REST API controller for questions + * Handle HTTP requests related to questions + */ +@RestController +@RequestMapping("/questions") +public class QuestionController { + + private final QuestionFacade questionFacade; + + /** + * Constructor for QuestionController + * + * @param questionFacade the service responsible for handling question-related logic + */ + @Autowired + public QuestionController(QuestionFacade questionFacade) { + this.questionFacade = questionFacade; + } + + /** + * Find a question by ID. + * + * @param id the ID of the question to find + * @return a QuestionDto object representing the found question + */ + @GetMapping("/{id}") + public QuestionDto find(@NotBlank @PathVariable String id) { + return questionFacade.find(id); + } + + /** + * Find questions by exercise ID and return them in a paginated format + * + * @param exerciseId the ID of the exercise to find questions for + * @param page the page number of the questions to retrieve + * @return a Result object containing a list of QuestionDto objects and pagination information + */ + @GetMapping("/exercise/{exercise-id}") + public Result<QuestionDto> findByExerciseId(@NotBlank @PathVariable("exercise-id") String exerciseId, + @PositiveOrZero @RequestParam int page) { + return questionFacade.findByExerciseId(exerciseId, page); + } + + /** + * Add a new question to an exercise + * + * @param dto a QuestionCreateDto object representing the new question to add + * @return a QuestionDto object representing the added question + * @throws ResponseStatusException if the exercise with exerciseId from QuestionCreateDto does not exist + */ + @PostMapping + public QuestionDto addQuestion(@Valid @RequestBody QuestionCreateDto dto) { + try { + return questionFacade.create(dto); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + e.getMessage()); + } + } + + /** + * Update question + * + * @param dto a QuestionDto object representing the updated question with correct id + * @return a QuestionUpdateDto object representing the updated question + * @throws ResponseStatusException if the question with id doesn't exist or its exercise doesn't exist + */ + @PutMapping("/{id}") + public QuestionDto updateQuestion(@NotBlank @PathVariable String id, @Valid @RequestBody QuestionUpdateDto dto) { + try { + return questionFacade.update(id, dto); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } catch (EntityNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + /** + * Add a new question to an exercise + * + * @param id of question to delete + */ + @DeleteMapping("/{id}") + public void deleteQuestion(@NotBlank @PathVariable String id) { + questionFacade.delete(id); + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..2f2cfc9f4219b69c9c475ecc70c6c4de428233a5 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java @@ -0,0 +1,126 @@ +package org.fuseri.moduleexercise.question; + +import org.fuseri.model.dto.common.Result; +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.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; + +/** + * Represent facade for managing questions + * Provide simplified interface for manipulating with questions + */ +@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, + AnswerService answerService, QuestionMapper questionMapper, + AnswerMapper answerMapper) { + this.questionService = questionService; + this.exerciseService = exerciseService; + this.answerService = answerService; + this.questionMapper = questionMapper; + this.answerMapper = answerMapper; + } + + /** + * Find a question by ID. + * + * @param id the ID of the question to find + * @return a QuestionDto object representing the found question + */ + public QuestionDto find(String id) { + var a = questionService.find(id); + return questionMapper.toDto(a); + } + + /** + * Find questions by exercise ID and return them in a paginated format + * + * @param exerciseId the ID of the exercise to find questions for + * @param page the page number of the questions to retrieve + * @return a Result object containing a list of QuestionDto objects and pagination information + */ + public Result<QuestionDto> findByExerciseId(String exerciseId, int page) { + Page<Question> questions = questionService.findByExerciseId(exerciseId, page); + return questionMapper.toResult(questions); + } + + /** + * Create a new question + * + * @param dto a QuestionCreateDto object representing the new question to add + * @return a QuestionDto object representing the added question + */ + public QuestionDto create(QuestionCreateDto dto) { + Question question = questionMapper.fromCreateDto(dto); + + Exercise exercise; + exercise = exerciseService.find(question.getExerciseId()); + + exercise.getQuestions().add(question); + question.setExerciseId(exercise.getId()); + + 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.setQuestionId(createdQuestion.getId()); + } + + return questionMapper.toDto(createdQuestion); + } + + /** + * Update question + * + * @param dto dto of updated question with correct id + * @return dto of updated question + */ + public QuestionDto update(String id, QuestionUpdateDto dto) { + Question question = questionMapper.fromUpdateDto(dto); + question.setId(id); + List<Answer> questionAnswers = answerService.findAllByQuestionId(id); + question.setAnswers(new HashSet<>(questionAnswers)); + Question updatedQuestion = questionService.update(question); + return questionMapper.toDto(updatedQuestion); + } + + /** + * Delete question + * + * @param id of qustion to delete + */ + public void delete(String 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 new file mode 100644 index 0000000000000000000000000000000000000000..9c4e62a2a7845c32941b219a539030fcdbf98a16 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java @@ -0,0 +1,26 @@ +package org.fuseri.moduleexercise.question; + +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.common.DomainMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * Mapper between Questions and their corresponding DTOs + */ +@Mapper +public interface QuestionMapper extends DomainMapper<Question, QuestionDto> { + + /** + * Convert DTO of type QuestionCreateDto to Question + * + * @param dto DTO to be converted + * @return corresponding Question entity created from DTO + */ + @Mapping(target = "answers", ignore = true) + Question fromCreateDto(QuestionCreateDto dto); + + Question fromUpdateDto(QuestionUpdateDto dto); +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..0f924cd5794fbfb300a826813de050a1dc7b4222 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java @@ -0,0 +1,20 @@ +package org.fuseri.moduleexercise.question; + +import org.fuseri.moduleexercise.common.DomainRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +/** + * A repository interface for managing Question entities + */ +public interface QuestionRepository extends DomainRepository<Question, String> { + + /** + * Find a page of questions associated with the exercise with the specified ID + * + * @param exerciseId the ID of the exercise to find questions for + * @param pageRequest the page request specifying the page number and page size + * @return a page of questions associated with the specified exercise + */ + Page<Question> findByExerciseId(String exerciseId, PageRequest pageRequest); +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..703b89dcd133afb330a3468fb0e24cc60d0e41d4 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java @@ -0,0 +1,36 @@ +package org.fuseri.moduleexercise.question; + +import org.fuseri.moduleexercise.common.DomainRepositoryImpl; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * An implementation of the QuestionRepository interface + * Provides access to Question entities stored in a data source + */ +@Repository +public class QuestionRepositoryImpl extends DomainRepositoryImpl<Question> implements QuestionRepository { + + /** + * Find a page of questions associated with the exercise with the specified ID + * + * @param exerciseId the ID of the exercise to find questions for + * @param pageRequest the page request specifying the page number and page size + * @return a page of questions associated with the specified exercise + */ + @Override + public Page<Question> findByExerciseId(String exerciseId, PageRequest pageRequest) { + List<Question> filteredQuestions = getItems() + .stream() + .filter(e -> e.getExerciseId().equals(exerciseId)) + .skip(pageRequest.getOffset()) + .limit(pageRequest.getPageSize()) + .toList(); + + return new PageImpl<>(filteredQuestions, pageRequest, filteredQuestions.size()); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..f30867aa5da680b2f61004a645c8fe1b21534161 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java @@ -0,0 +1,61 @@ +package org.fuseri.moduleexercise.question; + +import jakarta.persistence.EntityNotFoundException; +import lombok.Getter; +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; + +/** + * Represent a service for managing Question entities + */ +@Service +public class QuestionService extends DomainService<Question> { + + /** + * The repository instance used by this service + */ + @Getter + private final QuestionRepository repository; + + /** + * Construct a new instance of QuestionService with the specified repository + * + * @param repository the repository instance to be used by this service + */ + @Autowired + public QuestionService(QuestionRepository repository) { + this.repository = repository; + } + + /** + * Retrieve the Question entity with the specified ID + * + * @param id the ID of the Question entity to retrieve + * @return the Question entity with the specified ID + * @throws EntityNotFoundException if no Question entity exists with the specified ID + */ + @Transactional(readOnly = true) + public Question find(String id) { + return repository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Question '" + id + "' not found.")); + } + + /** + * Retrieve a page of Question entities associated with the specified exercise ID + * + * @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 + */ + @Transactional(readOnly = true) + public Page<Question> findByExerciseId(String exerciseId, int page) { + return repository.findByExerciseId( + exerciseId, + PageRequest.of(page, DomainService.DEFAULT_PAGE_SIZE)); + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..da00608e87053cae0b248e864e0a5797d816f9bc --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java @@ -0,0 +1,173 @@ +package org.fuseri.moduleexercise.answer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; +import org.fuseri.model.dto.exercise.AnswersCreateDto; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.fuseri.model.dto.exercise.QuestionCreateDto; +import org.fuseri.model.dto.exercise.QuestionDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import java.util.List; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; + +@SpringBootTest +@AutoConfigureMockMvc +public class AnswerTest { + + @Autowired + ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; + + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private QuestionDto createQuestion(String id) throws Exception { + var question = new QuestionCreateDto("this statement is false", id, + List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); + + + var posted = mockMvc.perform(post("/questions") + .content(asJsonString(question)) + .contentType(MediaType.APPLICATION_JSON)); + + var cont = posted.andReturn().getResponse().getContentAsString(); + var res = objectMapper.readValue(cont, QuestionDto.class); + return res; + } + + + private String createExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); + + String id = ""; + + try { + var dis = mockMvc.perform(post("/exercises") + .content(asJsonString(postExercise)) + .contentType(MediaType.APPLICATION_JSON)); + var ok = dis.andReturn().getResponse().getContentAsString(); + + var ll = objectMapper.readValue(ok, ExerciseDto.class); + + id = ll.getId(); + } catch (Exception e) { + assert (false); + } + return id; + } + + + @Test + void testCreateAnswer() throws Exception { + + List<AnswerDto> res = createAnswer(); + + var expected1 = new AnswerDto("True", false); + var expected2 = new AnswerDto("False", false); + + assert (res.get(0).equals(expected1)); + assert (res.get(1).equals(expected2)); + + } + + private List<AnswerDto> createAnswer() throws Exception { + var exerciseId = createExercise(); + var question = createQuestion(exerciseId); + + var incorrect1 = new AnswerInQuestionCreateDto("True", false); + var incorrect2 = new AnswerInQuestionCreateDto("False", false); + + var createAnswer = new AnswersCreateDto(question.getId(), List.of(incorrect1, incorrect2)); + + var posted = mockMvc.perform(post("/answers") + .content(asJsonString(createAnswer)) + .contentType(MediaType.APPLICATION_JSON)); + + var asStr = posted.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(asStr, new TypeReference<List<AnswerDto>>() { + }); + return res; + } + + + @Test + void getAnswer() throws Exception { + var exerciseId = createExercise(); + var question = createQuestion(exerciseId); + + var gets = mockMvc.perform(get(String.format("/answers/%s", question.getId()))); + + var content2 = gets.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(content2, new TypeReference<List<AnswerDto>>() { + }); + + assert (res.equals(question.getAnswers())); + + } + + @Test + void testUpdate() throws Exception { + + var exerciseId = createExercise(); + var question = createQuestion(exerciseId); + + var incorrect1 = new AnswerInQuestionCreateDto("True", false); + var incorrect2 = new AnswerInQuestionCreateDto("False", false); + + + var createAnswer = new AnswersCreateDto(question.getId(), List.of(incorrect1, incorrect2)); + + + var posted = mockMvc.perform(post("/answers") + .content(asJsonString(createAnswer)) + .contentType(MediaType.APPLICATION_JSON)); + + var asStr = posted.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(asStr, new TypeReference<List<AnswerDto>>() { + }); + + + var updated = """ + { + "text": "dis true", + "correct": false, + "questionId": "%s" + } + """; + + + updated = String.format(updated, question.getId()); + + var puts = mockMvc.perform(put(String.format("/answers/%s", res.get(0).getId())) + .content(updated).contentType(MediaType.APPLICATION_JSON)); + + var content = puts.andReturn().getResponse().getContentAsString(); + + var res2 = objectMapper.readValue(content, AnswerDto.class); + + var expected = new AnswerDto("dis true", false); + + assert res2.equals(expected); + + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a634b41941ff09a1d6fa1a82125c2015c3206bf1 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java @@ -0,0 +1,161 @@ +package org.fuseri.moduleexercise.exercise; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuseri.model.dto.common.Result; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + + +@SpringBootTest +@AutoConfigureMockMvc +public class ExerciseTest { + @Autowired + ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; + + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Test + void getExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); + + String id = ""; + + try { + var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); + var ok = dis.andReturn().getResponse().getContentAsString(); + + var ll = objectMapper.readValue(ok, ExerciseDto.class); + + id = ll.getId(); + } catch (Exception e) { + assert (false); + } + + + try { + var theId = String.format("/exercises/%s", id); + var smth = mockMvc.perform(get(theId)); + + } catch (Exception e) { + //do absolutely nothing + } + } + + @Test + void getFiltered() { + + + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 0, "0"); + var postExercise1 = new ExerciseCreateDto("idioms1", "exercise on basic idioms", 0, "0"); + var postExercise2 = new ExerciseCreateDto("idioms2", "exercise on basic idioms", 1, "0"); + + try { + var exercise1 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); + + var exercise2 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise1)).contentType(MediaType.APPLICATION_JSON)); + var exercise3 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise2)).contentType(MediaType.APPLICATION_JSON)); + } catch (Exception e) { + //do absolutly nothing + } + + + Map<String, String> params; + + try { + var filtered = mockMvc.perform(get("/exercises/filter").param("page", "0").param("courseId", "0").param("difficulty", "0")); + + var content = filtered.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(content, new TypeReference<Result<ExerciseDto>>() { + }); + + assert (res.getTotal() == 2); + } catch (Exception e) { + assert (false); + } + } + + @Test + void testCreateExercise() throws Exception { + var expectedResponse = new ExerciseDto(); + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); + + mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("idioms")).andExpect(jsonPath("$.description").value("exercise on basic idioms")).andExpect(jsonPath("$.difficulty").value(2)).andExpect(jsonPath("$.courseId").value("0")).andReturn().getResponse().getContentAsString(); + } + + @Test + void testUpdate() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); + + String id = ""; + + try { + var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); + var ok = dis.andReturn().getResponse().getContentAsString(); + + var ll = objectMapper.readValue(ok, ExerciseDto.class); + + id = ll.getId(); + } catch (Exception e) { + assert (false); + } + + var expectedExercise = new ExerciseDto(); + expectedExercise.setId(id); + expectedExercise.setName("idioms"); + expectedExercise.setDifficulty(2); + expectedExercise.setCourseId("idioms"); + expectedExercise.setDescription("exercise on basic idioms"); + + var content = """ + { + "name": "idioms", + "description": "exercise on basic idioms", + "difficulty": 2, + "courseId": "idioms" + } + """; + + + try { + var theId = String.format("/exercises/%s", id); + var dis = mockMvc.perform(put(theId).content(content).contentType(MediaType.APPLICATION_JSON)); + + var str = dis.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(str, ExerciseDto.class); + + assert res.equals(expectedExercise); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a099ff7c8cf058e033931222d5c76469ac40003e --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java @@ -0,0 +1,153 @@ +package org.fuseri.moduleexercise.question; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuseri.model.dto.common.Result; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.fuseri.model.dto.exercise.QuestionCreateDto; +import org.fuseri.model.dto.exercise.QuestionDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; + + +@SpringBootTest +@AutoConfigureMockMvc +public class QuestionTest { + + @Autowired + ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; + + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + void testCreateQuestion() throws Exception { + String id = createExercise(); + var answr = new AnswerDto("dis a logical paradox", true); + QuestionDto res = createQuestion(id); + var expected = new QuestionDto(); + expected.setAnswers(List.of(answr)); + expected.setExerciseId(id); + expected.setId(res.getId()); + expected.setText("this statement is false"); + + assert expected.equals(res); + } + + private QuestionDto createQuestion(String id) throws Exception { + var question = new QuestionCreateDto("this statement is false", id, List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); + + + var posted = mockMvc.perform(post("/questions").content(asJsonString(question)).contentType(MediaType.APPLICATION_JSON)); + + + var cont = posted.andReturn().getResponse().getContentAsString(); + var res = objectMapper.readValue(cont, QuestionDto.class); + return res; + } + + private String createExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); + + String id = ""; + + try { + var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); + var ok = dis.andReturn().getResponse().getContentAsString(); + + var ll = objectMapper.readValue(ok, ExerciseDto.class); + + id = ll.getId(); + } catch (Exception e) { + assert (false); + } + return id; + } + + + @Test + void getQuestion() throws Exception { + + + String exerciseId = createExercise(); + var question = createQuestion(exerciseId); + + var theId = String.format("/questions/%s", question.getId()); + + + var gets = mockMvc.perform(get(theId)); + + var content = gets.andReturn().getResponse().getContentAsString(); + var res = objectMapper.readValue(content, QuestionDto.class); + + assert res.equals(question); + + } + + @Test + void getByExercise() throws Exception { + + var exerciseId = createExercise(); + var question = createQuestion(exerciseId); + + var theId = String.format("/questions/exercise/%s", exerciseId); + + var smth = mockMvc.perform(get(theId).param("page", "0")); + + var content = smth.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(content, new TypeReference<Result<QuestionDto>>() { + }); + + Map<String, String> params; + + assert (res.getItems().get(0).equals(question)); + } + + @Test + void TestUpdate() throws Exception { + String id = createExercise(); + var question = createQuestion(id); + + var updated = """ + { + "text": "wat a paradox?", + "exerciseId": "%s" + } + """; + + updated = String.format(updated, id); + + var smth = mockMvc.perform(put(String.format("/questions/%s", question.getId())).content(updated).contentType(MediaType.APPLICATION_JSON)); + + var content = smth.andReturn().getResponse().getContentAsString(); + + var res = objectMapper.readValue(content, QuestionDto.class); + + question.setText("wat a paradox?"); + + assert (question.equals(res)); + + } +} diff --git a/application/pom.xml b/application/pom.xml index fbc2902e510c6776ba70a4695bbe1cf684610a9f..11c1c12c0a09887c996429d551d632c46f3260e1 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -72,6 +72,11 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>