diff --git a/README.md b/README.md index 3a2ad12470881fb0a2c87ede48ddca7de7ea52ae..b1befef99ae06ecc28324536ba87af69e813011b 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,37 @@ - Martin GargaloviÄŤ *@xgargal* - Jan PokornĂ˝ *@xpokorn8* - Ester VilĂmková - _Project Leader_ *@xvilimk* + + - **Assigment**: - Create a system for language school lecture management. Each lecture can occur on a given day and time and is related to some course. The course is defined by its (unique) name, language and proficiency level. However, each lecture will be independent. That means that each lecture can have a different topic. Different lecturers can give it, and an arbitrary number of students can enrol. Each lecturer will have a name, surname, and record of taught languages. In addition, the lecturer will indicate that they are a native speaker. Exercises can be prepared for each course to allow the students to learn the language. Each student can pick the exercises depending on the levels of difficulty. -- **Running the modules**: - - ```cd ./application``` - - ```mvn clean install``` - - ```cd ./module-*``` - - ```mvn spring-boot:run``` + +- **Running the modules using docker-compose**: + + ```console + cd ./application + mvn clean install + docker-compose build --parallel + docker-compose up + ``` + +- **Running the modules using podman-compose**: + ~~~console + cd ./application + mvn clean install + podman-compose build --parallel + podman-compose up + ~~~ + +- **Running the modules using docker**: + ~~~console + sudo docker run -d -p 5001:5001 xpokorn8/sprachschulsystem:certificate && + sudo docker run -d -p 5002:5002 xpokorn8/sprachschulsystem:exercise && + sudo docker run -d -p 5000:5000 xpokorn8/sprachschulsystem:language-school && + sudo docker run -d -p 5003:5003 xpokorn8/sprachschulsystem:mail + ~~~ + # Project Description diff --git a/application/docker-compose.yml b/application/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..41a6a0d3edb0fa3ffa8d88abb80419ffb7e96025 --- /dev/null +++ b/application/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + certificate: + build: ./module-certificate + container_name: certificate + image: xpokorn8/sprachschulsystem:certificate + ports: + - "5001:5001" + + exercise: + build: ./module-exercise + container_name: exercise + image: xpokorn8/sprachschulsystem:exercise + ports: + - "5002:5002" + + language-school: + build: ./module-language-school + container_name: language-school + image: xpokorn8/sprachschulsystem:language-school + ports: + - "5000:5000" + + mail: + build: ./module-mail + container_name: mail + image: xpokorn8/sprachschulsystem:mail + ports: + - "5003:5003" \ No newline at end of file diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java index 88995655d12e28abdf75cdd041d93d8d92ff7b8b..64c78eec16aa0a472883f9b1d705d8168d523b8f 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java @@ -28,10 +28,10 @@ public class CourseCreateDto { @NotNull(message = "Language type is required") @Valid - private LanguageTypeDto languageTypeDto; + private LanguageTypeDto language; @NotNull(message = "Proficiency level is required") @Valid - private ProficiencyLevelDto proficiencyLevelDto; + private ProficiencyLevelDto proficiency; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java index cf92a6127941045515111a6471bd08be283546b7..18e5695b056c1f3f477a05c5abd8f83bba32c9a1 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java @@ -20,7 +20,7 @@ import java.util.List; */ @Getter @Setter -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) public class CourseDto extends DomainObjectDto { @NotBlank(message = "Course name is required") @@ -33,22 +33,22 @@ public class CourseDto extends DomainObjectDto { @NotNull(message = "Language type is required") @Valid - private LanguageTypeDto languageTypeDto; + private LanguageTypeDto language; @NotNull(message = "Proficiency level is required") @Valid - private ProficiencyLevelDto proficiencyLevelDto; + private ProficiencyLevelDto proficiency; @NotNull(message = "Student's list is required") @Valid private List<Long> studentIds; - public CourseDto(String name, Integer capacity, LanguageTypeDto languageTypeDto, ProficiencyLevelDto proficiencyLevelDto) { - setId(0L); + public CourseDto(Long id, String name, Integer capacity, LanguageTypeDto languageTypeDto, ProficiencyLevelDto proficiencyLevelDto) { + this.setId(id); this.name = name; this.capacity = capacity; - this.languageTypeDto = languageTypeDto; - this.proficiencyLevelDto = proficiencyLevelDto; + this.language = languageTypeDto; + this.proficiency = proficiencyLevelDto; this.studentIds = new ArrayList<>(); } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java index 2b7f9fde7906edc96a7ccee7d2f926e5c83b3d0b..84214fd415d40511029aebd52f60bbc526ff8e7f 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java @@ -1,8 +1,5 @@ package org.fuseri.model.dto.course; -import lombok.AllArgsConstructor; - -@AllArgsConstructor public enum LanguageTypeDto { ENGLISH, GERMAN, 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 index 2418847a2d483585f0d0a57505f0cfd148aa2094..f7a845f6c9bc9970e3e8bcc4d1e2d6e2929c712a 100644 --- 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 @@ -15,6 +15,6 @@ public class AnswerCreateDto { @NotNull private boolean correct; - @NotNull - private long questionId; + @NotBlank + private String questionId; } 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 index f2c7da24058c59d4a600d9a7d7d0403652e46832..04996492928042452c248158a9d4cab9659a772a 100644 --- 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 @@ -1,7 +1,7 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Getter; @@ -11,8 +11,8 @@ import java.util.List; @Getter public class AnswersCreateDto { - @NotNull - private long questionId; + @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 index 21e7ee336865644ad6583481d791573688b8fb1e..332b343dce447081c05db0a98e2c57df8815602e 100644 --- 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 @@ -20,6 +20,6 @@ public class ExerciseCreateDto { @PositiveOrZero private int difficulty; - @NotNull - private long courseId; + @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 index db61e6bfd63120447bb638cf804c5c67d00ae3c6..ff7abd444839ba89d15b50e72f31a3973b62ceef 100644 --- 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 @@ -23,18 +23,28 @@ public class ExerciseDto extends DomainObjectDto { @PositiveOrZero private int difficulty; - @NotNull - private long courseId; + @NotBlank + private String courseId; @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof ExerciseDto that)) return false; - return getDifficulty() == that.getDifficulty() && getCourseId() == that.getCourseId() && Objects.equals(getName(), that.getName()) && Objects.equals(getDescription(), that.getDescription()); + 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() { - return Objects.hash(getName(), getDescription(), getDifficulty(), getCourseId()); + 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 index cd215815305d8bb06de5b81e47f6b6e8380954d2..738a9031739586351ba52b993c7522e0dbcb226d 100644 --- 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 @@ -2,7 +2,6 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; @@ -15,8 +14,8 @@ public class QuestionCreateDto { @NotBlank private String text; - @NotNull - private long exerciseId; + @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 index 9e62c5a4d92d5f4e089a4635278755c2172d5025..5cca20b14b60c9361eaea0893ff874500b690c61 100644 --- 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 @@ -2,7 +2,6 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import org.fuseri.model.dto.common.DomainObjectDto; @@ -17,8 +16,8 @@ public class QuestionDto extends DomainObjectDto { @NotBlank private String text; - @NotNull - private long exerciseId; + @NotBlank + private String exerciseId; @Valid private List<AnswerDto> answers; @@ -26,12 +25,20 @@ public class QuestionDto extends DomainObjectDto { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof QuestionDto that)) return false; - return getExerciseId() == that.getExerciseId() && Objects.equals(getText(), that.getText()) && Objects.equals(getAnswers(), that.getAnswers()); + 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() { - return Objects.hash(getText(), getExerciseId(), getAnswers()); + 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 index 59e0fda131a540c0c08da981fd36d156a49cb5ae..2efa520493c718e108a180fabae12a4887913cab 100644 --- 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 @@ -1,7 +1,6 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; @@ -12,6 +11,6 @@ public class QuestionUpdateDto { @NotBlank private String text; - @NotNull - private long exerciseId; + @NotBlank + private String exerciseId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java index 6fc9b766fefb23e0f31aa2d79af7f30dc9ae7ab2..0279180b41ea671a944ab270735ac5f5c00acb75 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java @@ -3,7 +3,6 @@ package org.fuseri.model.dto.lecture; import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; -import org.fuseri.model.dto.course.CourseDto; import java.time.LocalDateTime; @@ -27,10 +26,10 @@ public class LectureCreateDto { @Min(value = 1, message = "Lecture capacity must be at least 1") private Integer capacity; - @NotBlank(message = "Lecture course cannot be blank") - private String courseId; + @NotNull(message = "Lecture course cannot be null") + private Long courseId; - public LectureCreateDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, String courseId) { + public LectureCreateDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, Long courseId) { this.from = from; this.to = to; this.topic = topic; diff --git a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java index f54ac383238e7e6cfbecbde6a71af275f4a4efb0..df35d83cfa5668cb6c9d28b514d50221f6425584 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java @@ -29,16 +29,16 @@ public class LectureDto extends DomainObjectDto { @Min(value = 1, message = "Lecture capacity must be at least 1") private Integer capacity; - @NotNull(message = "Lecture capacity cannot be null") - private String lecturerId; + @NotNull(message = "Lecture lecturer cannot be null") + private Long lecturerId; - @NotBlank(message = "Lecture courseId cannot be blank") - private String courseId; + @NotNull(message = "Lecture courseId cannot be null") + private Long courseId; @NotNull(message = "Student IDs list cannot be null") private List<Long> studentIds; - public LectureDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, String lecturerId, String courseId) { + public LectureDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, Long lecturerId, Long courseId) { this.from = from; this.to = to; this.topic = topic; diff --git a/application/module-certificate/Dockerfile b/application/module-certificate/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..21e346d921cf9c19dd93c4fc3c5d29555c2aa59a --- /dev/null +++ b/application/module-certificate/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-certificate-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java index 254e194e328afb5e2a1f502b0855b438b6f69a12..d0643d2529989fed6a1bd9a671d27df285cb2d2b 100644 --- a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java @@ -2,7 +2,9 @@ package org.fuseri.modulecertificate.service; import jakarta.validation.Valid; import org.fuseri.model.dto.certificate.CertificateCreateDto; +import org.fuseri.model.dto.certificate.CertificateDto; import org.fuseri.model.dto.certificate.CertificateSimpleDto; +import org.fuseri.model.dto.common.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java index b019490cf36ce9294670ff15d6e1628ca9c45507..20a1ec68448a8b0d97f6373a006c25708151a01e 100644 --- a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java @@ -9,12 +9,16 @@ import org.fuseri.model.dto.course.ProficiencyLevelDto; import org.fuseri.model.dto.user.AddressDto; import org.fuseri.model.dto.user.UserDto; import org.junit.jupiter.api.Test; +import org.springdoc.core.converters.models.Pageable; 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.data.domain.PageRequest; 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.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -99,14 +103,14 @@ class CertificateControllerTests { .andExpect(status().is4xxClientError()); } -// @Test -// void findCertificateIdForUserAndCourse() throws Exception { -// mockMvc.perform(get("/certificates/findForUserAndCourse") -// .param("userId", "0") -// .param("courseId", "0")) -// .andExpect(status().isOk()) -// .andExpect(content().string("[]")); -// } + @Test + void findCertificateIdForUserAndCourse() throws Exception { + mockMvc.perform(get("/certificates/findForUserAndCourse") + .param("userId", "0") + .param("courseId", "0")) + .andExpect(status().isOk()) + .andExpect(content().string("[]")); + } @Test void findCertificateIdWithoutUserId() throws Exception { @@ -149,10 +153,10 @@ class CertificateControllerTests { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); -// mockMvc.perform(get("/certificates/findAll") -// .content("{ \"page\": 0, \"size\": 1, \"sort\": []}") -// .contentType(MediaType.APPLICATION_JSON)) -// .andExpect(status().isOk()); + mockMvc.perform(get("/certificates/findAll") + .content("{ \"page\": 0, \"size\": 1, \"sort\": []}") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); } @Test diff --git a/application/module-exercise/Dockerfile b/application/module-exercise/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e31eb6c17a52ef6e28c4edbf02bed322397ab46 --- /dev/null +++ b/application/module-exercise/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-exercise-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] 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 index 9686c42ea06b4286bfdb2b7c1324cef02121761f..12fac75aedf87d7c1f29fe7333a24762d19a9ad3 100644 --- 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 @@ -1,9 +1,7 @@ package org.fuseri.moduleexercise.answer; -import jakarta.persistence.*; import lombok.*; import org.fuseri.moduleexercise.common.DomainObject; -import org.fuseri.moduleexercise.question.Question; /** * Represent Answer entity @@ -14,16 +12,10 @@ import org.fuseri.moduleexercise.question.Question; @NoArgsConstructor @AllArgsConstructor @Builder -@Entity -@Table(name = "answer") public class Answer extends DomainObject { - private String text; - @Column(name = "is_correct") private boolean correct; - @ManyToOne - @JoinColumn(name="question_id") - private Question question; + 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 index ebdc9cd6e97752ea388ec0755f44f74b6f857bd4..9f2f75a06870be2aa1bb7898a0caa471197f1681 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 @@ -1,17 +1,13 @@ package org.fuseri.moduleexercise.answer; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +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.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -32,24 +28,30 @@ public class AnswerController { 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 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 + * @return an AnswerDto object representing the newly created answer + * @throws ResponseStatusException if the question ID specified in the dto does not exist */ - @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) { + public List<AnswerDto> createMultiple(@Valid @RequestBody AnswersCreateDto dto) { try { - return ResponseEntity.status(HttpStatus.CREATED).body(facade.createMultiple(dto)); + return facade.createMultiple(dto); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } } @@ -58,42 +60,30 @@ public class AnswerController { * * @param id of answer to update * @param dto dto with updated answer information - * @return A ResponseEntity with an AnswerDto object representing the updated answer on an HTTP status code of 200 if the update was successful. - * or a NOT_FOUND response if the answer ID is invalid + * @throws ResponseStatusException if the question id specified in the AnswerCreateDto dto does not exist */ - @Operation(summary = "Update an answer", description = "Updates an answer with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Answer with the specified ID updated successfully."), - @ApiResponse(responseCode = "400", description = "Invalid input."), - @ApiResponse(responseCode = "404", description = "Answer with the specified ID was not found.") - }) @PutMapping("/{id}") - public ResponseEntity<AnswerDto> update(@NotNull @PathVariable long id, @Valid @RequestBody AnswerCreateDto dto) { + public AnswerDto update(@NotBlank @PathVariable String id, @Valid @RequestBody AnswerCreateDto dto) { try { - return ResponseEntity.ok(facade.update(id, dto)); + return facade.update(id, dto); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + e.getMessage()); } } /** - * Delete answer with the specified ID + * Delete answer with the given id * * @param id of answer to delete * @throws ResponseStatusException if answer with specified id does not exist */ - @Operation(summary = "Delete an answer with specified ID", description = "Deletes an answer with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Answer with the specified ID deleted successfully."), - @ApiResponse(responseCode = "404", description = "Answer with the specified ID was not found.") - }) @DeleteMapping("/{id}") - public ResponseEntity<Void> delete(@NotNull @PathVariable long id) { + public void delete(@NotBlank @PathVariable String id) { try { facade.delete(id); - return ResponseEntity.noContent().build(); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + 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 index 370eed4b5a45b4505768faf1d8396d0385d4d26b..9ef573dca41c6d90e1396484a1c54e29b1730073 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 @@ -1,6 +1,5 @@ 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; @@ -18,7 +17,6 @@ import java.util.List; * Provide simplified interface for manipulating with answers */ @Service -@Transactional public class AnswerFacade { private final AnswerService answerService; private final QuestionService questionService; @@ -39,6 +37,16 @@ public class AnswerFacade { 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 * @@ -52,7 +60,7 @@ public class AnswerFacade { question = questionService.find(dto.getQuestionId()); Answer answer = mapper.fromCreateDto(answerDto); - answer.setQuestion(question); + answer.setQuestionId(question.getId()); var createdAnswer = answerService.create(answer); question.getAnswers().add(answer); createdAnswers.add(createdAnswer); @@ -67,7 +75,7 @@ public class AnswerFacade { * @param id of answer to update * @param dto dto with updated answer information */ - public AnswerDto update(long id, AnswerCreateDto dto) { + public AnswerDto update(String id, AnswerCreateDto dto) { var updatedAnswer = mapper.fromCreateDto(dto); updatedAnswer.setId(id); answerService.update(updatedAnswer); @@ -76,7 +84,7 @@ public class AnswerFacade { question = questionService.find(dto.getQuestionId()); var questionAnswers = question.getAnswers(); - questionAnswers.removeIf(a -> a.getId() == id); + questionAnswers.removeIf(a -> a.getId().equals(id)); questionAnswers.add(updatedAnswer); question.setAnswers(questionAnswers); questionService.update(question); @@ -89,14 +97,14 @@ public class AnswerFacade { * * @param id of answer to delete */ - public void delete(long id) { + public void delete(String id) { var answer = answerService.find(id); Question question; - question = questionService.find(answer.getQuestion().getId()); + question = questionService.find(answer.getQuestionId()); var questionAnswers = question.getAnswers(); - questionAnswers.removeIf(a -> a.getId() == answer.getId()); + 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/AnswerRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java index 8d0844a898915658d73220923586a5630ac351a7..7f1918988b192f6c0f0f5fe54f92544d4d72dcbf 100644 --- 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 @@ -1,16 +1,14 @@ package org.fuseri.moduleexercise.answer; -import org.springframework.data.jpa.repository.JpaRepository; +import org.fuseri.moduleexercise.common.DomainRepository; import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; import java.util.List; /** * A repository interface for managing Answer entities */ -@Repository -public interface AnswerRepository extends JpaRepository<Answer, Long> { +public interface AnswerRepository extends DomainRepository<Answer, String> { /** * Find all answers to a question with the specified ID @@ -18,5 +16,5 @@ public interface AnswerRepository extends JpaRepository<Answer, Long> { * @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") long questionId); + 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 index f5346fc5993874b4de7ca7d49fe39ba8c17ee6ba..593656acfea40265f835d91d6b5f2572a8bdea31 100644 --- 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 @@ -36,13 +36,9 @@ public class AnswerService extends DomainService<Answer> { * * @param questionId the ID of the question to retrieve answers for * @return a list of Answer entities with the specified question ID - * @throws EntityNotFoundException if question with questionId does not exist */ @Transactional(readOnly = true) - public List<Answer> findAllByQuestionId(long questionId) { - if (!getRepository().existsById(questionId)) { - throw new EntityNotFoundException("Question with id " + questionId + " not found."); - } + public List<Answer> findAllByQuestionId(String questionId) { return repository.findByQuestionId(questionId); } @@ -54,7 +50,7 @@ public class AnswerService extends DomainService<Answer> { * @throws EntityNotFoundException if no Answer entity exists with the specified id */ @Transactional(readOnly = true) - public Answer find(long id) { + 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/DomainObject.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java index aae4e7759835653b19afad27db311e8d23095515..077a8e5cdb521f4c233572accf0e6a8bb10ac4d3 100644 --- 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 @@ -1,12 +1,12 @@ package org.fuseri.moduleexercise.common; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; 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. */ @@ -16,7 +16,6 @@ import lombok.Setter; public abstract class DomainObject { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long 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 index ec9d96818a5ba660d0e49c85e5437fc298d5a732..badd21263def20ee0cf660c2be3f1f156a84aed4 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 @@ -1,8 +1,5 @@ package org.fuseri.moduleexercise.common; -import jakarta.persistence.EntityNotFoundException; -import org.springframework.data.jpa.repository.JpaRepository; - /** * Represent common service for managing entities * @@ -20,7 +17,7 @@ public abstract class DomainService<T extends DomainObject> { * * @return the repository used by this service */ - public abstract JpaRepository<T, Long> getRepository(); + public abstract DomainRepository<T, String> getRepository(); /** * Create an entity by saving it to the repository @@ -39,17 +36,14 @@ public abstract class DomainService<T extends DomainObject> { * @return the updated entity */ public T update(T entity) { - if (!getRepository().existsById(entity.getId())) { - throw new EntityNotFoundException("Entity with id " + entity.getId() + " not found."); - } - return getRepository().save(entity); + return getRepository().update(entity); } /** * Delete an entity with specified id * @param id id of the entity to delete */ - public void delete(long id) { + 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 index 6070b8a70b26a36f3e87b4f4661a43a64625cfe5..65ff0e59f9a31773168eaf378000bac45f010fd0 100644 --- 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 @@ -1,6 +1,5 @@ package org.fuseri.moduleexercise.exercise; -import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -19,8 +18,6 @@ import java.util.Set; @Setter @NoArgsConstructor @Builder -@Entity -@Table(name = "exercise") public class Exercise extends DomainObject { private String name; @@ -29,10 +26,8 @@ public class Exercise extends DomainObject { private int difficulty; - @Column(name = "lecture_id") - private long courseId; + private String courseId; - @OneToMany(mappedBy="exercise", cascade = CascadeType.ALL) private Set<Question> questions = new HashSet<>(); /** @@ -44,7 +39,7 @@ public class Exercise extends DomainObject { * @param courseId id of lecture to which exercise belongs * @param questions question exercise contains */ - public Exercise(String name, String description, int difficulty, long courseId, Set<Question> questions) { + public Exercise(String name, String description, int difficulty, String courseId, Set<Question> questions) { this.name = name; this.description = description; this.difficulty = difficulty; 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 f8d6926a570172791747800a2b7b920d122f49d7..1a53e6f9630c53ba29d16275f982202628181fbf 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 @@ -1,20 +1,17 @@ package org.fuseri.moduleexercise.exercise; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +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.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.server.ResponseStatusException; /** * Represent a REST API controller for exercises @@ -24,70 +21,55 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/exercises") public class ExerciseController { - private final ExerciseFacade facade; + private final ExerciseService service; + + private final ExerciseMapper mapper; /** - * Constructor for ExerciseController + * Constructor for AnswerController * - * @param facade the facade responsible for handling exercise-related logic + * @param service the service responsible for handling exercise-related logic + * @param mapper the mapper responsible for converting between DTOs and entities */ @Autowired - public ExerciseController(ExerciseFacade facade) { - this.facade = facade; + public ExerciseController(ExerciseService service, ExerciseMapper mapper) { + this.service = service; + this.mapper = mapper; } /** - * Create a new exercise + * Create a new answer for the given question ID * - * @param dto containing information about the exercise to create - * @return a ResponseEntity containing an ExerciseDto object representing the newly created exercise + * @param dto the ExerciseCreateDto object containing information about the exercise to create + * @return an ExerciseDto object representing the newly created exercise */ - @Operation(summary = "Create an exercise", description = "Creates a new exercise.") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Exercise created successfully."), - @ApiResponse(responseCode = "400", description = "Invalid input.") - }) @PostMapping - public ResponseEntity<ExerciseDto> create(@Valid @RequestBody ExerciseCreateDto dto) { - ExerciseDto exerciseDto = facade.create(dto); - return ResponseEntity.status(HttpStatus.CREATED).body(exerciseDto); + 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 a ResponseEntity containing an ExerciseDto object representing the found exercise, or a 404 Not Found response - * if the exercise with the specified ID was not found + * @return an ExerciseDto object representing the found exercise */ - @Operation(summary = "Get an exercise by ID", description = "Returns an exercise with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Exercise with the specified ID retrieved successfully."), - @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") - }) @GetMapping("/{id}") - public ResponseEntity<ExerciseDto> find(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(facade.find(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + public ExerciseDto find(@NotBlank @PathVariable String id) { + return mapper.toDto(service.find(id)); } /** - * Find exercises and return them in paginated format + * Find exercises and return them in a 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 Result object containing a list of ExerciseDto objects and pagination information */ - @Operation(summary = "Get exercises in paginated format", description = "Returns exercises in paginated format.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved paginated exercises"), - @ApiResponse(responseCode = "400", description = "Invalid page number supplied"), - }) @GetMapping - public ResponseEntity<Result<ExerciseDto>> findAll(@PositiveOrZero @RequestParam int page) { - return ResponseEntity.ok(facade.findAll(page)); + public Result<ExerciseDto> findAll(@PositiveOrZero @RequestParam int page) { + Page<Exercise> exercise = service.findAll(page); + return mapper.toResult(exercise); } /** @@ -96,81 +78,47 @@ public class ExerciseController { * @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 + * @return a Result object containing a list of filtered ExerciseDto objects woth pagination information */ - @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( - @PositiveOrZero @RequestParam int page, @NotNull @RequestParam long courseId, + public Result<ExerciseDto> findPerDifficultyPerCourse( + @PositiveOrZero @RequestParam int page, @NotBlank @RequestParam String courseId, @PositiveOrZero @RequestParam int difficulty) { - Result<ExerciseDto> exercises = facade.findPerDifficultyPerCourse(page, courseId, difficulty); - return ResponseEntity.ok(exercises); - } - - /** - * 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 ResponseEntity containing a Result object with a list of QuestionDto objects and pagination information, - * or a NOT_FOUND response if the exercise ID is invalid - */ - @Operation(summary = "Find questions belonging to exercise by exercise ID", - description = "Returns a paginated list of questions for the specified exercise ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Questions found and returned successfully."), - @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) { - try { - Result<QuestionDto> questions = facade.getQuestions(exerciseId, page); - return ResponseEntity.ok(questions); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + Page<Exercise> exercise = service.findPerDifficultyPerCourse(page, courseId, difficulty); + return mapper.toResult(exercise); } /** - * Update an exercise with ID + * 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 A ResponseEntity with an ExerciseDto object representing the updated exercise an HTTP status code of 200 if the update was successful. - * or a NOT_FOUND response if the exercise ID is invalid + * @return an ExerciseDto object representing the updated exercise + * @throws ResponseStatusException invalid exercise id */ - @Operation(summary = "Update a exercise", description = "Updates a exercise with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Exercise with the specified ID updated successfully."), - @ApiResponse(responseCode = "400", description = "Invalid input."), - @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") - }) + @PutMapping("/{id}") - public ResponseEntity<ExerciseDto> update(@NotNull @PathVariable long id, @Valid @RequestBody ExerciseCreateDto dto) { + public ExerciseDto update(@NotBlank @PathVariable String id, @Valid @RequestBody ExerciseCreateDto dto) { + Exercise exercise = mapper.fromCreateDto(dto); + exercise.setId(id); + try { - return ResponseEntity.ok(facade.update(id, dto)); + return mapper.toDto(service.update(exercise)); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } } /** - * Delete an exercise with ID + * Delete an exercise with exerciseId * * @param id the ID of the exercise to delete */ - @Operation(summary = "Delete a exercise with specified ID", description = "Deletes a exercise with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Exercise with the specified ID deleted successfully."), - }) @DeleteMapping("/{id}") - public ResponseEntity<Void> delete(@NotNull @PathVariable long id) { - facade.delete(id); - return ResponseEntity.noContent().build(); + public void delete(@NotBlank @PathVariable String id) { + service.delete(id); } } 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 abdc8f0134c814e883965402167611342cbf9d33..76ec2a3a1eaf05eb979ccd4cdd8d5267397addb3 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,19 +1,14 @@ package org.fuseri.moduleexercise.exercise; import org.fuseri.model.dto.common.Result; -import org.fuseri.moduleexercise.question.Question; +import org.fuseri.moduleexercise.common.DomainRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; /** * A repository interface for managing Exercise entities */ -@Repository -public interface ExerciseRepository extends JpaRepository<Exercise, Long> { +public interface ExerciseRepository extends DomainRepository<Exercise, String> { /** * Filters the exercises by the specified difficulty level and course id, @@ -25,9 +20,5 @@ public interface ExerciseRepository extends JpaRepository<Exercise, Long> { * @param difficulty the difficulty level to filter by * @return a {@link Result} 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); - - @Query("SELECT q FROM Exercise e JOIN e.questions q WHERE e.id = :exerciseId") - Page<Question> getQuestions(PageRequest pageRequest, @Param("exerciseId") Long exerciseId); + 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 index 6d819b6869f97d526db56fa295418df8e2f3a676..677c704a7892b13672c8534c0d731d3ba7a2abb6 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 @@ -3,7 +3,6 @@ package org.fuseri.moduleexercise.exercise; import jakarta.persistence.EntityNotFoundException; import lombok.Getter; import org.fuseri.moduleexercise.common.DomainService; -import org.fuseri.moduleexercise.question.Question; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -40,7 +39,7 @@ public class ExerciseService extends DomainService<Exercise> { * @throws EntityNotFoundException if no Exercise entity exists with the specified ID */ @Transactional(readOnly = true) - public Exercise find(long id) { + public Exercise find(String id) { return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Exercise '" + id + "' not found.")); } @@ -64,25 +63,8 @@ public class ExerciseService extends DomainService<Exercise> { * @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, long courseId, int difficulty) { + public Page<Exercise> findPerDifficultyPerCourse(int page, String courseId, int difficulty) { return repository.filterPerDifficultyPerCourse( PageRequest.of(page, DEFAULT_PAGE_SIZE), courseId, difficulty); } - - /** - * 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> getQuestions(long exerciseId, int page) { - if (!repository.existsById(exerciseId)) { - throw new EntityNotFoundException("Exercise with ID " + exerciseId + "does not exist."); - } - return repository.getQuestions( - PageRequest.of(page, DomainService.DEFAULT_PAGE_SIZE), - exerciseId); - } } 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..042604af6ad1e339be34b38f5481806938dfbb38 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,12 +1,14 @@ package org.fuseri.moduleexercise.question; -import jakarta.persistence.*; -import lombok.*; +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; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -15,19 +17,25 @@ import java.util.Set; @Getter @Setter @NoArgsConstructor -@AllArgsConstructor @Builder -@Entity -@Table(name = "question") public class Question extends DomainObject { private String text; - @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Answer> answers = new HashSet<>(); - @ManyToOne - @JoinColumn(name = "exercise_id", nullable=false) - private Exercise exercise; + 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 index 1595f03f02f77ac6474c9399d88647f843a2b7d8..bd217cc1c00f36d02b1b074b6e9028c20e291220 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 @@ -1,23 +1,17 @@ package org.fuseri.moduleexercise.question; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import org.fuseri.model.dto.exercise.AnswerDto; +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.http.ResponseEntity; import org.springframework.web.bind.annotation.*; - -import java.util.List; +import org.springframework.web.server.ResponseStatusException; /** * Represent a REST API controller for questions @@ -43,64 +37,40 @@ public class QuestionController { * Find a question by ID. * * @param id the ID of the question to find - * @return a ResponseEntity containing a QuestionDto object representing the found question, or a 404 Not Found response - * if the question with the specified ID was not found + * @return a QuestionDto object representing the found question */ - @Operation(summary = "Get a question by ID", description = "Returns a question with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Question with the specified ID retrieved successfully.", - content = @Content(schema = @Schema(implementation = QuestionDto.class))), - @ApiResponse(responseCode = "404", description = "Question with the specified ID was not found.") - }) @GetMapping("/{id}") - public ResponseEntity<QuestionDto> find(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(questionFacade.find(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + public QuestionDto find(@NotBlank @PathVariable String id) { + return questionFacade.find(id); } /** - * Retrieve a list of AnswerDto objects which belong to the question with ID + * Find questions by exercise ID and return them in a paginated format * - * @param id the ID of the question for which to retrieve answers - * @return a ResponseEntity containing a List of AnswerDto objects, or a 404 Not Found response - * if the question with the specified ID was not found + * @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 */ - @Operation(summary = "Retrieve answers for a specific question") - @ApiResponse(responseCode = "200", description = "Successfully retrieved answers", - content = @Content(schema = @Schema(implementation = AnswerDto.class))) - @ApiResponse(responseCode = "404", description = "Question not found") - @GetMapping("/{id}/answers") - public ResponseEntity<List<AnswerDto>> getQuestionAnswers(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(questionFacade.getQuestionAnswers(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + @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 ResponseEntity containing a QuestionDto object representing the posted question, or a 404 Not Found response - * if the exercise with the specified ID in dto was not found + * @return a QuestionDto object representing the added question + * @throws ResponseStatusException if the exercise with exerciseId from QuestionCreateDto does not exist */ - @Operation(summary = "Add a new question with answers to an exercise", description = "Creates a new question with answers and associates it with the specified exercise.") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Question with answers created and added to the exercise successfully.", - content = @Content(schema = @Schema(implementation = QuestionDto.class))), - @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") - }) @PostMapping - public ResponseEntity<QuestionDto> addQuestion(@Valid @RequestBody QuestionCreateDto dto) { + public QuestionDto addQuestion(@Valid @RequestBody QuestionCreateDto dto) { try { - QuestionDto createdQuestionDto = questionFacade.create(dto); - return ResponseEntity.status(HttpStatus.CREATED).body(createdQuestionDto); + return questionFacade.create(dto); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + e.getMessage()); } } @@ -108,35 +78,27 @@ public class QuestionController { * Update question * * @param dto a QuestionDto object representing the updated question with correct id - * @return a ResponseEntity containing a QuestionUpdateDto object representing the updated question, - * or a 404 Not Found response if the question with the specified ID was not found + * @return a QuestionUpdateDto object representing the updated question + * @throws ResponseStatusException if the question with id doesn't exist or its exercise doesn't exist */ - @Operation(summary = "Update a question by ID", description = "Updates a question with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Question with the specified ID updated successfully."), - @ApiResponse(responseCode = "404", description = "Question with the specified ID was not found.") - }) @PutMapping("/{id}") - public ResponseEntity<QuestionDto> updateQuestion(@NotNull @PathVariable long id, @Valid @RequestBody QuestionUpdateDto dto) { + public QuestionDto updateQuestion(@NotBlank @PathVariable String id, @Valid @RequestBody QuestionUpdateDto dto) { try { - return ResponseEntity.ok(questionFacade.update(id, dto)); + return questionFacade.update(id, dto); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } } /** - * Delete question with ID from exercise + * Add a new question to an exercise * * @param id of question to delete */ - @Operation(summary = "Delete a question with specified ID", description = "Deletes a question with the specified ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Question with the specified ID deleted successfully."), - }) @DeleteMapping("/{id}") - public ResponseEntity<Void> deleteQuestion(@NotNull @PathVariable long id) { + public void deleteQuestion(@NotBlank @PathVariable String id) { questionFacade.delete(id); - return ResponseEntity.noContent().build(); } } \ 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 bf6f97d1b2b7f3273d8157731b4a4cfb37247fc3..2f2cfc9f4219b69c9c475ecc70c6c4de428233a5 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 @@ -1,7 +1,6 @@ package org.fuseri.moduleexercise.question; -import jakarta.transaction.Transactional; -import org.fuseri.model.dto.exercise.AnswerDto; +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; @@ -11,6 +10,7 @@ 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; @@ -20,7 +20,6 @@ import java.util.List; * Represent facade for managing questions * Provide simplified interface for manipulating with questions */ -@Transactional @Service public class QuestionFacade { private final QuestionService questionService; @@ -47,19 +46,21 @@ public class QuestionFacade { * @param id the ID of the question to find * @return a QuestionDto object representing the found question */ - public QuestionDto find(long id) { + public QuestionDto find(String id) { var a = questionService.find(id); return questionMapper.toDto(a); } /** - * Retrieve a list of AnswerDto objects which belong to question with questionId + * Find questions by exercise ID and return them in a paginated format * - * @param questionId the ID of the question for which to retrieve answers - * @return a List of AnswerDto objects + * @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 List<AnswerDto> getQuestionAnswers(long questionId) { - return answerMapper.toDtoList(answerService.findAllByQuestionId(questionId)); + public Result<QuestionDto> findByExerciseId(String exerciseId, int page) { + Page<Question> questions = questionService.findByExerciseId(exerciseId, page); + return questionMapper.toResult(questions); } /** @@ -72,10 +73,10 @@ public class QuestionFacade { Question question = questionMapper.fromCreateDto(dto); Exercise exercise; - exercise = exerciseService.find(dto.getExerciseId()); + exercise = exerciseService.find(question.getExerciseId()); exercise.getQuestions().add(question); - question.setExercise(exercise); + question.setExerciseId(exercise.getId()); var answerDtos = dto.getAnswers(); var answers = new HashSet<Answer>(); @@ -89,7 +90,7 @@ public class QuestionFacade { var createdQuestion = questionService.create(question); for (var answer : answers) { - answer.setQuestion(createdQuestion); + answer.setQuestionId(createdQuestion.getId()); } return questionMapper.toDto(createdQuestion); @@ -101,7 +102,7 @@ public class QuestionFacade { * @param dto dto of updated question with correct id * @return dto of updated question */ - public QuestionDto update(long id, QuestionUpdateDto dto) { + public QuestionDto update(String id, QuestionUpdateDto dto) { Question question = questionMapper.fromUpdateDto(dto); question.setId(id); List<Answer> questionAnswers = answerService.findAllByQuestionId(id); @@ -115,7 +116,7 @@ public class QuestionFacade { * * @param id of qustion to delete */ - public void delete(long id) { + public void delete(String id) { var question = questionService.find(id); for (var answer : question.getAnswers()) { answerService.delete(answer.getId()); 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 index b5b2adc4d3208451e1d1789654def49121feb14f..0f924cd5794fbfb300a826813de050a1dc7b4222 100644 --- 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 @@ -1,11 +1,20 @@ package org.fuseri.moduleexercise.question; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; +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 */ -@Repository -public interface QuestionRepository extends JpaRepository<Question, Long> { +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 index 2d54e3d1f1ae0bcc2492626e70919af7e493520c..f30867aa5da680b2f61004a645c8fe1b21534161 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 @@ -39,8 +39,23 @@ public class QuestionService extends DomainService<Question> { * @throws EntityNotFoundException if no Question entity exists with the specified ID */ @Transactional(readOnly = true) - public Question find(long id) { + 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/main/resources/application.properties b/application/module-exercise/src/main/resources/application.properties index 22a97362c389b6439b4b17bb0c4e7d144ce4f1a5..ec3c390e0877b6c499b7aade0abc0111d719d865 100644 --- a/application/module-exercise/src/main/resources/application.properties +++ b/application/module-exercise/src/main/resources/application.properties @@ -1,3 +1 @@ -server.port=5002 -spring.h2.console.enabled=true -spring.datasource.url=jdbc:h2:mem:exercices \ No newline at end of file +server.port=5002 \ No newline at end of file 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 index 46f2097e2410bb935a7591e2d09213791faa9884..da00608e87053cae0b248e864e0a5797d816f9bc 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -36,7 +37,7 @@ public class AnswerTest { } } - private QuestionDto createQuestion(long id) throws Exception { + 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))); @@ -51,10 +52,10 @@ public class AnswerTest { } - private long createExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0); + private String createExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - long id = 0L; + String id = ""; try { var dis = mockMvc.perform(post("/exercises") @@ -105,6 +106,23 @@ public class AnswerTest { 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 { @@ -137,18 +155,18 @@ public class AnswerTest { """; -// 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); + 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 index 92481d501ab4189e69bc0659fad3b6eed609d353..a634b41941ff09a1d6fa1a82125c2015c3206bf1 100644 --- 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 @@ -41,9 +41,9 @@ public class ExerciseTest { @Test void getExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0L); + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - long id = 0L; + String id = ""; try { var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); @@ -70,9 +70,9 @@ public class ExerciseTest { void getFiltered() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 0, 0L); - var postExercise1 = new ExerciseCreateDto("idioms1", "exercise on basic idioms", 0, 0L); - var postExercise2 = new ExerciseCreateDto("idioms2", "exercise on basic idioms", 1, 0L); + 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)); @@ -100,39 +100,19 @@ public class ExerciseTest { } } -// @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 testCreateExercise() throws Exception { var expectedResponse = new ExerciseDto(); - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0L); + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isCreated()).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(); + 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, 0L); + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - long id = 0L; + String id = ""; try { var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); @@ -149,7 +129,7 @@ public class ExerciseTest { expectedExercise.setId(id); expectedExercise.setName("idioms"); expectedExercise.setDifficulty(2); - expectedExercise.setCourseId(0L); + expectedExercise.setCourseId("idioms"); expectedExercise.setDescription("exercise on basic idioms"); var content = """ @@ -157,13 +137,13 @@ public class ExerciseTest { "name": "idioms", "description": "exercise on basic idioms", "difficulty": 2, - "courseId": 0 + "courseId": "idioms" } """; try { - var theId = String.format("/exercises/%d", id); + 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(); 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 index c248a76380962a53889db274582d0d499827ead1..a099ff7c8cf058e033931222d5c76469ac40003e 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -16,9 +17,11 @@ 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 @@ -40,7 +43,7 @@ public class QuestionTest { @Test void testCreateQuestion() throws Exception { - long id = createExercise(); + String id = createExercise(); var answr = new AnswerDto("dis a logical paradox", true); QuestionDto res = createQuestion(id); var expected = new QuestionDto(); @@ -49,10 +52,10 @@ public class QuestionTest { expected.setId(res.getId()); expected.setText("this statement is false"); -// assert expected.equals(res); + assert expected.equals(res); } - private QuestionDto createQuestion(long id) throws Exception { + 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))); @@ -64,10 +67,10 @@ public class QuestionTest { return res; } - private long createExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0L); + private String createExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - long id = 0L; + String id = ""; try { var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); @@ -87,7 +90,7 @@ public class QuestionTest { void getQuestion() throws Exception { - long exerciseId = createExercise(); + String exerciseId = createExercise(); var question = createQuestion(exerciseId); var theId = String.format("/questions/%s", question.getId()); @@ -103,24 +106,28 @@ public class QuestionTest { } @Test - void getAnswer() throws Exception { + void getByExercise() throws Exception { + var exerciseId = createExercise(); var question = createQuestion(exerciseId); - var gets = mockMvc.perform(get(String.format("/questions/%s/answers", question.getId()))); + var theId = String.format("/questions/exercise/%s", exerciseId); + + var smth = mockMvc.perform(get(theId).param("page", "0")); - var content2 = gets.andReturn().getResponse().getContentAsString(); + var content = smth.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(content2, new TypeReference<List<AnswerDto>>() { + var res = objectMapper.readValue(content, new TypeReference<Result<QuestionDto>>() { }); - assert (res.equals(question.getAnswers())); + Map<String, String> params; + assert (res.getItems().get(0).equals(question)); } @Test void TestUpdate() throws Exception { - long id = createExercise(); + String id = createExercise(); var question = createQuestion(id); var updated = """ @@ -131,16 +138,16 @@ public class QuestionTest { """; 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); + 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?"); + question.setText("wat a paradox?"); -// assert (question.equals(res)); + assert (question.equals(res)); } } diff --git a/application/module-language-school/Dockerfile b/application/module-language-school/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f2be765c3d8637d93aba9fefea412f0b0ae2edf1 --- /dev/null +++ b/application/module-language-school/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-language-school-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/module-language-school/pom.xml b/application/module-language-school/pom.xml index 42e3a5ded4aed76f99a3ae2be4c55d304e05b0d0..d54d174bf358a4ab6e62a74c5f219f2661ab89f4 100644 --- a/application/module-language-school/pom.xml +++ b/application/module-language-school/pom.xml @@ -59,8 +59,17 @@ <version>0.0.1-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-tx</artifactId> + </dependency> + <dependency> + <groupId>io.swagger</groupId> + <artifactId>swagger-annotations</artifactId> + <version>1.6.9</version> + </dependency> - </dependencies> + </dependencies> <build> <plugins> diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java index f56b95e001eb1ae13ed4093509cb4ad354c81b4a..078871d947aaa180bd7a5f6549354cadd7b2eade 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java @@ -7,8 +7,6 @@ import jakarta.persistence.MappedSuperclass; import lombok.Getter; import lombok.Setter; -import java.util.UUID; - @Getter @Setter @MappedSuperclass @@ -16,5 +14,6 @@ public abstract class DomainObject { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id;} + private Long id; +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..2227c5640bba11c5405bc103d2cc4ddbd9f67c8e --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java @@ -0,0 +1,22 @@ +package org.fuseri.modulelanguageschool.common; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException() { + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java index 38f42dfa4b571e5e77cb362df20d89fc73d038ac..99e1ff83bc6f5d1ffcaffcd2689327a4e29e1ff9 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java @@ -1,13 +1,17 @@ package org.fuseri.modulelanguageschool.course; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; +import jakarta.persistence.*; import lombok.*; import org.fuseri.modulelanguageschool.common.DomainObject; +import org.fuseri.modulelanguageschool.user.User; + +import java.util.List; +import java.util.Set; @Getter @Setter +@Entity +@Table(name = "course") @NoArgsConstructor @AllArgsConstructor public class Course extends DomainObject { @@ -19,5 +23,16 @@ public class Course extends DomainObject { private Language language; @Enumerated(EnumType.STRING) - private ProficiencyLevel proficiencyLevel; + private ProficiencyLevel proficiency; + + @ManyToMany + private Set<User> students; + + public void enrolStudent(User student) { + students.add(student); + } + + public void expelStudent(User student) { + students.remove(student); + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java index 9646fb8ff0c88dc974b065f545f17f365c7c0ded..353dcd43d30b280c8cf683fe794faeb0c29d01b9 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java @@ -1,26 +1,36 @@ package org.fuseri.modulelanguageschool.course; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import jakarta.validation.Valid; -import jakarta.validation.constraints.PositiveOrZero; import org.fuseri.model.dto.course.CourseCreateDto; import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.course.LanguageTypeDto; import org.fuseri.model.dto.course.ProficiencyLevelDto; import org.fuseri.model.dto.user.UserDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.List; /** * This class represents a RESTful controller for Courses resources. * It handles incoming HTTP requests related to courses, and delegates them to the appropriate service method. */ +@Api(tags = "Course Controller") @RestController @RequestMapping("/courses") public class CourseController { - public static final String COURSE_NAME = "english b2 course"; + private final CourseFacade courseFacade; + + @Autowired + public CourseController(CourseFacade courseFacade) { + this.courseFacade = courseFacade; + } /** * Creates a new course. @@ -28,9 +38,10 @@ public class CourseController { * @param dto the CourseCreateDto containing the course data * @return the newly created CourseDto */ - @PostMapping("/create") + @ApiOperation(value = "Create a new course") + @PostMapping public CourseDto create(@Valid @RequestBody CourseCreateDto dto) { - return new CourseDto(dto.getName(), dto.getCapacity(), dto.getLanguageTypeDto(), dto.getProficiencyLevelDto()); + return courseFacade.create(dto); } /** @@ -39,9 +50,10 @@ public class CourseController { * @param id the ID of the course to retrieve * @return the CourseDto for the specified ID */ + @ApiOperation(value = "Retrieve a course by ID") @GetMapping("/find/{id}") - public CourseDto find(@PathVariable String id) { - return new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + public CourseDto find(@PathVariable Long id) { + return courseFacade.findById(id); } /** @@ -50,36 +62,36 @@ public class CourseController { * @param page the page number to retrieve * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses") @GetMapping("/findAll") - public List<CourseDto> findAll(@RequestParam int page) { - return new ArrayList<>(); + public Page<CourseDto> findAll(@RequestParam int page) { + return courseFacade.findAll(PageRequest.of(page, 10, Sort.by(Sort.Direction.ASC, "name"))); } /** * Retrieves a paginated list of courses of a given language * - * @param page the page number to retrieve * @param lang the language to find courses of * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses of a given language") @GetMapping("/findAllByLang") - public List<CourseDto> findAll(@RequestParam int page, @RequestParam LanguageTypeDto lang) { - return new ArrayList<>(); + public List<CourseDto> findAll(@RequestParam LanguageTypeDto lang) { + return courseFacade.findAll(lang); } /** * Retrieves a paginated list of courses of a given language and proficiency * - * @param page the page number to retrieve * @param lang the language to find courses of * @param prof the proficiency of the language * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses of a given language and proficiency") @GetMapping("/findAllByLangProf") - public List<CourseDto> findAll(@RequestParam int page, - @RequestParam LanguageTypeDto lang, + public List<CourseDto> findAll(@RequestParam LanguageTypeDto lang, @RequestParam ProficiencyLevelDto prof) { - return new ArrayList<>(); + return courseFacade.findAll(lang, prof); } /** @@ -89,19 +101,21 @@ public class CourseController { * @param dto the CourseCreateDto containing the updated course data * @return the updated CourseDto */ + @ApiOperation(value = "Update an existing course") @PutMapping("/update/{id}") - public CourseDto update(@PathVariable String id, @Valid @RequestBody CourseCreateDto dto) { - return new CourseDto(dto.getName(), dto.getCapacity(), dto.getLanguageTypeDto(), dto.getProficiencyLevelDto()); + public CourseDto update(@PathVariable Long id, @Valid @RequestBody CourseCreateDto dto) { + return courseFacade.update(id, dto); } /** * Deletes a course by ID. * * @param id the ID of the course to delete - * @return true if the course was successfully deleted, false otherwise */ + @ApiOperation(value = "Delete a course by ID") @DeleteMapping("/delete/{id}") - public void delete(@PathVariable String id) { + public void delete(@PathVariable Long id) { + courseFacade.delete(id); } @@ -112,11 +126,10 @@ public class CourseController { * @param student UserDto for the student * @return the CourseDto representing the updated course */ + @ApiOperation(value = "Add student to the existing course") @PatchMapping("/enrol/{id}") - public CourseDto enrol(@PathVariable String id, @RequestBody UserDto student) { - var course = new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); - course.setStudentIds(new ArrayList<>(List.of(student.getId()))); - return course; + public CourseDto enrol(@PathVariable Long id, @RequestBody UserDto student) { + return courseFacade.enrol(id, student); } /** @@ -126,9 +139,10 @@ public class CourseController { * @param student UserDto for the student * @return the CourseDto representing the updated course */ + @ApiOperation(value = "Remove student from the existing course") @PatchMapping("/expel/{id}") - public CourseDto expel(@PathVariable String id, @RequestBody UserDto student) { - return new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + public CourseDto expel(@PathVariable Long id, @RequestBody UserDto student) { + return courseFacade.expel(id, student); } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..db7f28aac1051afbb68b097701e87d5b7cd916d5 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java @@ -0,0 +1,73 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class CourseFacade { + private final CourseService courseService; + private final CourseMapper courseMapper; + private final UserMapper userMapper; + + @Autowired + public CourseFacade(CourseService courseService, CourseMapper courseMapper, UserMapper userMapper) { + this.courseService = courseService; + this.courseMapper = courseMapper; + this.userMapper = userMapper; + } + + @Transactional + public CourseDto create(CourseCreateDto dto) { + return courseMapper.mapToDto(courseService.save(courseMapper.mapToCourse(dto))); + } + + @Cacheable(cacheNames = "courses", key = "#id") + @Transactional(readOnly = true) + public CourseDto findById(Long id) { + return courseMapper.mapToDto(courseService.findById(id)); + } + + @Transactional(readOnly = true) + public Page<CourseDto> findAll(Pageable pageable) { + return courseMapper.mapToPageDto(courseService.findAll(pageable)); + } + + @Transactional + public CourseDto update(Long id, CourseCreateDto dto) { + return courseMapper.mapToDto(courseService.update(id, courseMapper.mapToCourse(dto))); + } + + @Transactional + public void delete(Long id) { + courseService.delete(id); + } + + public List<CourseDto> findAll(LanguageTypeDto lang) { + return courseMapper.mapToList(courseService.findAll(Language.valueOf(lang.name()))); + } + + public List<CourseDto> findAll(LanguageTypeDto lang, ProficiencyLevelDto prof) { + return courseMapper.mapToList(courseService.findAll(Language.valueOf(lang.name()), ProficiencyLevel.valueOf(prof.name()))); + } + + public CourseDto enrol(Long id, UserDto student) { + return courseMapper.mapToDto(courseService.enrol(id, userMapper.fromDto(student))); + } + + public CourseDto expel(Long id, UserDto student) { + return courseMapper.mapToDto(courseService.expel(id, userMapper.fromDto(student))); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..9a5bf7c032fb11df53ef65ddad42b0e82230a7bf --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java @@ -0,0 +1,27 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CourseMapper { + + CourseDto mapToDto(Course course); + + Course mapToCourse(CourseDto courseDto); + + List<CourseDto> mapToList(List<Course> persons); + + + default Page<CourseDto> mapToPageDto(Page<Course> courses) { + return new PageImpl<>(mapToList(courses.getContent()), courses.getPageable(), courses.getTotalPages()); + } + + Course mapToCourse(CourseCreateDto dto); +} + diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..4ea630cc6ecdf2f0d68624041e1f529758f6082f --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java @@ -0,0 +1,17 @@ +package org.fuseri.modulelanguageschool.course; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CourseRepository extends JpaRepository<Course, Long> { + + @Query("SELECT c FROM Course c WHERE c.language = ?1") + List<Course> findAllByLang(Language language); + + @Query("SELECT c FROM Course c WHERE c.language = ?1 AND c.proficiency = ?2") + List<Course> findAllByLangProf(Language language, ProficiencyLevel proficiencyLevel); +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java new file mode 100644 index 0000000000000000000000000000000000000000..13079299f90503bc9f66ca889e05cd59ae6473f0 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java @@ -0,0 +1,89 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.modulelanguageschool.common.ResourceNotFoundException; +import org.fuseri.modulelanguageschool.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +public class CourseService { + + private final CourseRepository courseRepository; + + @Autowired + public CourseService(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + } + + @Transactional + public Course save(Course course) { + return courseRepository.save(course); + } + + @Transactional(readOnly = true) + public Course findById(Long id) { + return courseRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Course with id: " + id + " was not found.")); + } + + @Transactional(readOnly = true) + public Page<Course> findAll(Pageable pageable) { + return courseRepository.findAll(pageable); + } + + @Transactional + public Course update(Long id, Course newCourse) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.setName(newCourse.getName()); + course.setCapacity(newCourse.getCapacity()); + course.setLanguage(newCourse.getLanguage()); + course.setProficiency(newCourse.getProficiency()); + return courseRepository.save(course); + } else { + throw new ResourceNotFoundException("Course with id: " + id + " was not found."); + } + } + + @Transactional + public void delete(Long id) { + courseRepository.deleteById(id); + } + + public List<Course> findAll(Language language) { + return courseRepository.findAllByLang(language); + } + + public List<Course> findAll(Language language, ProficiencyLevel proficiencyLevel) { + return courseRepository.findAllByLangProf(language, proficiencyLevel); + } + + public Course enrol(Long id, User student) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.enrolStudent(student); + return courseRepository.save(course); + } else { + throw new ResourceNotFoundException("Course with id: " + id + " was not found."); + } + } + + public Course expel(Long id, User student) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.expelStudent(student); + return courseRepository.save(course); + } else { + throw new ResourceNotFoundException("Course with id: " + id + " was not found."); + } + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java index 86b9670a578674a85557aad6768c022561a1b53a..a719f242bad99bdd3349d44cdddbbc51fc2319fa 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java @@ -3,6 +3,7 @@ package org.fuseri.modulelanguageschool.lecture; import jakarta.persistence.Entity; import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,13 +13,15 @@ import org.fuseri.modulelanguageschool.course.Course; import org.fuseri.modulelanguageschool.user.User; import java.time.LocalDateTime; -import java.util.List; +import java.util.Set; /** * This class represents a lecture entity in the domain model. */ @Getter @Setter +@Entity +@Table(name = "lecture") @NoArgsConstructor @AllArgsConstructor public class Lecture extends DomainObject { @@ -27,9 +30,20 @@ public class Lecture extends DomainObject { private LocalDateTime to; private String topic; + @ManyToOne private Course course; + @ManyToOne private User lecturer; - private List<User> user; + @ManyToMany + private Set<User> students; + + public void enrolStudent(User student) { + students.add(student); + } + + public void expelStudent(User student) { + students.remove(student); + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java index 34d91578d4a8f53d4dbc75972d3216cbb199adf6..0922a5e2f19edf7f6ce206fb6c4fddf8f50276d5 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java @@ -1,28 +1,32 @@ package org.fuseri.modulelanguageschool.lecture; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import jakarta.validation.Valid; -import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.lecture.LectureCreateDto; import org.fuseri.model.dto.lecture.LectureDto; import org.fuseri.model.dto.user.UserDto; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.time.Month; -import java.util.ArrayList; import java.util.List; /** * This class represents a RESTful controller for Lecture resources. * It handles incoming HTTP requests related to lectures, and delegates them to the appropriate service method. */ +@Api(tags = "Lecture Controller") @RestController @RequestMapping("/lectures") public class LectureController { - private static final LocalDateTime START_DATETIME = LocalDateTime.of(2045, Month.JUNE, 30, 12, 0, 0); - private static final LocalDateTime END_DATETIME = LocalDateTime.of(2045, Month.JUNE, 30, 14, 0, 0); - public static final String TOPIC = "Learning how to spell deprecated"; + private final LectureFacade lectureFacade; + + @Autowired + public LectureController(LectureFacade lectureFacade) { + this.lectureFacade = lectureFacade; + } + /** * Creates a new lecture resource by delegating to the LectureService's create method. @@ -30,20 +34,22 @@ public class LectureController { * @param lecture the LectureDto representing the lecture to be created * @return the LectureDto representing the newly created lecture */ - @PostMapping("/create") + @ApiOperation(value = "Create a new lecture") + @PostMapping public LectureDto create(@Valid @RequestBody LectureCreateDto lecture) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + return lectureFacade.create(lecture); } /** * Retrieves a lecture resource by its ID. * - * @param id the ID of the lecture to find + * @param courseId the ID of the lecture to find * @return the LectureDto representing the found lecture */ - @GetMapping("find/{id}") - public LectureDto find(@PathVariable String id) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiOperation(value = "Retrieve a lecture by its ID") + @GetMapping("find/{courseId}") + public LectureDto find(@PathVariable Long courseId) { + return lectureFacade.findById(courseId); } /** @@ -52,9 +58,10 @@ public class LectureController { * @param courseId the course to retrieve lectures from * @return the list of LectureDtos */ + @ApiOperation(value = "Retrieve a list of lectures for the corresponding course") @GetMapping("/findByCourse") - public List<LectureDto> findByCourse(@Valid @RequestParam String courseId) { - return new ArrayList<>(); + public List<LectureDto> findByCourse(@Valid @RequestParam Long courseId) { + return lectureFacade.findAll(courseId); } /** @@ -63,9 +70,10 @@ public class LectureController { * @param lecture the CourseCreateDto representing the updated lecture * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Update an existing lecture") @PutMapping("/update/{id}") - public LectureDto update(@PathVariable String id, @Valid @RequestBody LectureCreateDto lecture) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + public LectureDto update(@PathVariable Long id, @Valid @RequestBody LectureCreateDto lecture) { + return lectureFacade.update(id, lecture); } /** @@ -73,21 +81,24 @@ public class LectureController { * * @param id the ID of the lecture to delete */ + @ApiOperation(value = "Delete a lecture by its ID") @DeleteMapping("/delete/{id}") - public void delete(@PathVariable String id) { + public void delete(@PathVariable Long id) { + lectureFacade.delete(id); } /** * Adds lecturer to the existing lecture resource * - * @param id id of lecture to update - * @param lecturer UserDto for the course lecturer + * @param id id of lecture to update + * @param lecturerDto UserDto for the course lecturer * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Add lecturer to the existing lecture") @PatchMapping("/setLecturer/{id}") - public LectureDto setLecturer(@PathVariable String id, @RequestBody UserDto lecturer) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + public LectureDto setLecturer(@PathVariable Long id, @RequestBody UserDto lecturerDto) { + return lectureFacade.setLecturer(id, lecturerDto); } /** @@ -97,11 +108,10 @@ public class LectureController { * @param student UserDto for the course student * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Add student to the existing lecture") @PatchMapping("/enrol/{id}") - public LectureDto enrol(@PathVariable String id, @RequestBody UserDto student) { - var lecture = new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); - lecture.setStudentIds(new ArrayList<>(List.of(student.getId()))); - return lecture; + public LectureDto enrol(@PathVariable Long id, @RequestBody UserDto student) { + return lectureFacade.enrol(id, student); } /** @@ -111,8 +121,9 @@ public class LectureController { * @param student UserDto for the course student * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Remove student from the existing lecture") @PatchMapping("/expel/{id}") - public LectureDto expel(@PathVariable String id, @RequestBody UserDto student) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + public LectureDto expel(@PathVariable Long id, @RequestBody UserDto student) { + return lectureFacade.expel(id, student); } } \ No newline at end of file diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..08fe710ce9f4fc519a3dd1996d2ea1d99ed33066 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java @@ -0,0 +1,78 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class LectureFacade { + + private final LectureService lectureService; + private final LectureMapper lectureMapper; + private final UserMapper userMapper; + + @Autowired + public LectureFacade(LectureService lectureService, LectureMapper lectureMapper, UserMapper userMapper) { + this.lectureService = lectureService; + this.lectureMapper = lectureMapper; + this.userMapper = userMapper; + } + + @Transactional + public LectureDto create(LectureCreateDto dto) { + return lectureMapper.mapToDto(lectureService.save(lectureMapper.mapToLecture(dto))); + } + + @Cacheable(cacheNames = "courses", key = "#id") + @Transactional(readOnly = true) + public LectureDto findById(Long id) { + return lectureMapper.mapToDto(lectureService.findById(id)); + } + + @Transactional(readOnly = true) + public List<LectureDto> findAll(Long id) { + return lectureMapper.mapToList(lectureService.findAllByCourse(id)); + } + + @Transactional + public LectureDto update(Long id, LectureCreateDto dto) { + return lectureMapper.mapToDto(lectureService.update(id, lectureMapper.mapToLecture(dto))); + } + + @Transactional + public void delete(Long id) { + lectureService.delete(id); + } + + public List<LectureDto> findAll(LanguageTypeDto lang) { + return lectureMapper.mapToList(lectureService.findAll(Language.valueOf(lang.name()))); + } + + public List<LectureDto> findAll(LanguageTypeDto lang, ProficiencyLevelDto prof) { + return lectureMapper.mapToList(lectureService.findAll(Language.valueOf(lang.name()), ProficiencyLevel.valueOf(prof.name()))); + } + + public LectureDto enrol(Long id, UserDto student) { + return lectureMapper.mapToDto(lectureService.enrol(id, userMapper.fromDto(student))); + } + + public LectureDto expel(Long id, UserDto student) { + return lectureMapper.mapToDto(lectureService.expel(id, userMapper.fromDto(student))); + } + + public LectureDto setLecturer(Long id, UserDto lecturerDto) { + return lectureMapper.mapToDto(lectureService.setLecturer(id, userMapper.fromDto(lecturerDto))); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..8136a3e21de60635099c9284cfc71d2dec40ee2b --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java @@ -0,0 +1,25 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface LectureMapper { + LectureDto mapToDto(Lecture lecture); + + Lecture mapToLecture(LectureDto lectureDto); + + List<LectureDto> mapToList(List<Lecture> lectures); + + + default Page<LectureDto> mapToPageDto(Page<Lecture> lectures) { + return new PageImpl<>(mapToList(lectures.getContent()), lectures.getPageable(), lectures.getTotalPages()); + } + + Lecture mapToLecture(LectureCreateDto dto); +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..1fd35bce8e435fd2bb321504daabf03772997fc3 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java @@ -0,0 +1,21 @@ +package org.fuseri.modulelanguageschool.lecture; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LectureRepository extends JpaRepository<Lecture, Long> { + + @Query("SELECT l FROM Lecture l WHERE l.course.id = ?1") + List<Lecture> findAllByCourse(Long id); + + @Query("SELECT l FROM Lecture l WHERE l.course.language = ?1") + List<Lecture> findAllByLang(Language language); + + @Query("SELECT l FROM Lecture l WHERE l.course.language = ?1 AND l.course.proficiency = ?2") + List<Lecture> findAllByLangProf(Language language, ProficiencyLevel proficiencyLevel); +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java new file mode 100644 index 0000000000000000000000000000000000000000..a9e90bcbae837c0785c4119e76f6d548163c19e7 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java @@ -0,0 +1,102 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.modulelanguageschool.common.ResourceNotFoundException; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +public class LectureService { + + private final LectureRepository lectureRepository; + + @Autowired + public LectureService(LectureRepository lectureRepository) { + this.lectureRepository = lectureRepository; + } + + @Transactional + public Lecture save(Lecture lecture) { + return lectureRepository.save(lecture); + } + + @Transactional(readOnly = true) + public Lecture findById(Long id) { + return lectureRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Lecture with id: " + id + " was not found.")); + } + + @Transactional(readOnly = true) + public List<Lecture> findAllByCourse(Long id) { + return lectureRepository.findAllByCourse(id); + } + + @Transactional + public Lecture update(Long id, Lecture newLecture) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.setFrom(newLecture.getFrom()); + lecture.setTo(newLecture.getTo()); + lecture.setTopic(newLecture.getTopic()); + lecture.setCourse(newLecture.getCourse()); + lecture.setLecturer(newLecture.getLecturer()); + lecture.setStudents(newLecture.getStudents()); + return lectureRepository.save(lecture); + } else { + throw new ResourceNotFoundException("Lecture with id: " + id + " was not found."); + } + } + + @Transactional + public void delete(Long id) { + lectureRepository.deleteById(id); + } + + public List<Lecture> findAll(Language language) { + return lectureRepository.findAllByLang(language); + } + + public List<Lecture> findAll(Language language, ProficiencyLevel proficiencyLevel) { + return lectureRepository.findAllByLangProf(language, proficiencyLevel); + } + + public Lecture enrol(Long id, User student) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.enrolStudent(student); + return lectureRepository.save(lecture); + } else { + throw new ResourceNotFoundException("Lecture with id: " + id + " was not found."); + } + } + + public Lecture expel(Long id, User student) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.expelStudent(student); + return lectureRepository.save(lecture); + } else { + throw new ResourceNotFoundException("Lecture with id: " + id + " was not found."); + } + } + + public Lecture setLecturer(Long id, User lecturer) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.setLecturer(lecturer); + return lectureRepository.save(lecture); + } else { + throw new ResourceNotFoundException("Lecture with id: " + id + " was not found."); + } + } +} diff --git a/application/module-language-school/src/main/resources/application.properties b/application/module-language-school/src/main/resources/application.properties index 888dcec44dfa5c7cece2f03962df19175a89349e..08d8fcc59c97de1773876715a96a126e0831521e 100644 --- a/application/module-language-school/src/main/resources/application.properties +++ b/application/module-language-school/src/main/resources/application.properties @@ -1 +1,13 @@ -server.port=5000 \ No newline at end of file +server.port=5000 + +spring.jpa.open-in-view=false +spring.datasource.url=jdbc:h2:mem:social-network;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=SedaQ-app +spring.datasource.password=$argon2id$v=19$m=16,t=2,p=1$YmF0bWFuYmF0bWFu$MdHYB359HdivAb9J6CaILw +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +# showing SQL is generally good practice for running project locally to check whether there is not an issue with implementation of JPA methods. +spring.jpa.show-sql=true +spring.jackson.property-naming-strategy=LOWER_CAMEL_CASE +spring.cache.type=NONE +appconfig.enablecache=false \ No newline at end of file diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java index d57ba5c02bfbfedea4eb52dca7f2cef0d13e4c1c..ec5cff89f979d0424fc118be02aea6f3c420c7c1 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java @@ -14,6 +14,7 @@ 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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -21,7 +22,8 @@ import java.util.ArrayList; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -29,27 +31,36 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringBootTest @AutoConfigureMockMvc public class CourseTest { + + private final CourseCreateDto courseCreateDto = new CourseCreateDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + private final CourseDto courseDto = new CourseDto(0L, "english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); @Autowired ObjectMapper objectMapper; @Autowired private MockMvc mockMvc; + @MockBean private CourseController courseController; - private final CourseCreateDto courseCreateDto = new CourseCreateDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2);; - private final CourseDto courseDto = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } @Test void createCourse() throws Exception { Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create") + mockMvc.perform(post("/courses") .content(asJsonString(courseCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) .andExpect(jsonPath("$.id").exists()) .andReturn().getResponse().getContentAsString(); } @@ -59,7 +70,7 @@ public class CourseTest { CourseCreateDto invalidCourseCreateDto = new CourseCreateDto(null, null, null, null); Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create") + mockMvc.perform(post("/courses") .content(asJsonString(invalidCourseCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); @@ -68,27 +79,27 @@ public class CourseTest { @Test void createCourseWithoutParameter() throws Exception { Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create")) + mockMvc.perform(post("/courses")) .andExpect(status().is4xxClientError()); } @Test void findCourse() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(courseController.find(id)).thenReturn(courseDto); mockMvc.perform(get("/courses/find/" + id)) .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id)) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.id").value(id)) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) .andReturn().getResponse().getContentAsString(); } @Test void findCourseWithoutId() throws Exception { - Mockito.when(courseController.find(ArgumentMatchers.anyString())).thenReturn(courseDto); + Mockito.when(courseController.find(ArgumentMatchers.anyLong())).thenReturn(courseDto); mockMvc.perform(get("/courses/find/")) .andExpect(status().is4xxClientError()); } @@ -96,18 +107,18 @@ public class CourseTest { @Test void findAll() throws Exception { int page = 0; - Mockito.when(courseController.findAll(page)).thenReturn(new ArrayList<>()); + Mockito.when(courseController.findAll(page)).thenReturn(new PageImpl<>(new ArrayList<>())); String response = mockMvc.perform(get("/courses/findAll").param("page", Integer.toString(page))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - assertThat("response", response, is("[]")); + assertTrue(response.contains("\"content\":[]")); } @Test void findAllWithoutPage() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt())).thenReturn(new ArrayList<>()); - mockMvc.perform(get("/courses/findAll")) +// Mockito.when(courseController.findAll(ArgumentMatchers.anyInt())).thenReturn(new PageImpl<>(new ArrayList<>())); + mockMvc.perform(get("/courses/findAll")) .andExpect(status().is4xxClientError()); } @@ -115,7 +126,7 @@ public class CourseTest { void findAllByLang() throws Exception { int page = 0; LanguageTypeDto lang = LanguageTypeDto.ENGLISH; - Mockito.when(courseController.findAll(page, lang)).thenReturn(new ArrayList<>()); + Mockito.when(courseController.findAll(lang)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/courses/findAllByLang") .param("page", Integer.toString(page)) .param("lang", lang.toString())) @@ -125,33 +136,18 @@ public class CourseTest { assertThat("response", response, is("[]")); } - @Test - void findAllByLangWithoutParameters() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class))) - .thenReturn(new ArrayList<>()); - mockMvc.perform(get("/courses/findAllByLang")) - .andExpect(status().is4xxClientError()); - } - @Test void findAllByLangWithoutLang() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class))) - .thenReturn(new ArrayList<>()); - String page = "0"; - mockMvc.perform(get("/courses/findAllByLang").param("page", page)) + mockMvc.perform(get("/courses/findAllByLang")) .andExpect(status().is4xxClientError()); } @Test void findAllByLangProf() throws Exception { - int page = 0; LanguageTypeDto lang = LanguageTypeDto.ENGLISH; ProficiencyLevelDto proficiencyLevel = ProficiencyLevelDto.A1; - Mockito.when(courseController.findAll(page, lang, proficiencyLevel)).thenReturn(new ArrayList<>()); + Mockito.when(courseController.findAll(lang, proficiencyLevel)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/courses/findAllByLangProf") - .param("page", Integer.toString(page)) .param("lang", lang.toString()) .param("prof", proficiencyLevel.toString())) .andExpect(status().isOk()) @@ -162,7 +158,7 @@ public class CourseTest { @Test void findAllByLangProfWithoutParameters() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), + Mockito.when(courseController.findAll( ArgumentMatchers.isA(LanguageTypeDto.class), ArgumentMatchers.isA(ProficiencyLevelDto.class))) .thenReturn(new ArrayList<>()); @@ -172,7 +168,7 @@ public class CourseTest { @Test void findAllByLangProfWithoutLangProf() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), + Mockito.when(courseController.findAll( ArgumentMatchers.isA(LanguageTypeDto.class), ArgumentMatchers.isA(ProficiencyLevelDto.class))) .thenReturn(new ArrayList<>()); @@ -183,26 +179,26 @@ public class CourseTest { @Test void updateCourse() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(courseController.update(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(CourseCreateDto.class))) + ArgumentMatchers.isA(CourseCreateDto.class))) .thenReturn(courseDto); mockMvc.perform(put("/courses/update/" + id) - .content(asJsonString(courseDto)) - .contentType(MediaType.APPLICATION_JSON)) + .content(asJsonString(courseDto)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) .andExpect(jsonPath("$.id").value(courseDto.getId())) .andReturn().getResponse().getContentAsString(); } @Test void updateCourseWithoutParameter() throws Exception { - Mockito.when(courseController.update(ArgumentMatchers.anyString(), + Mockito.when(courseController.update(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(CourseCreateDto.class))) .thenReturn(courseDto); mockMvc.perform(put("/courses/update")) @@ -211,7 +207,7 @@ public class CourseTest { @Test void deleteCourse() throws Exception { - String id = "0"; + Long id = 0L; Mockito.doNothing().when(courseController).delete(id); mockMvc.perform(delete("/courses/delete/" + id)) @@ -220,7 +216,7 @@ public class CourseTest { @Test void deleteCourseWithoutParameter() throws Exception { - Mockito.doNothing().when(courseController).delete(ArgumentMatchers.anyString()); + Mockito.doNothing().when(courseController).delete(ArgumentMatchers.anyLong()); mockMvc.perform(delete("/courses/delete/")) .andExpect(status().is4xxClientError()); @@ -228,11 +224,11 @@ public class CourseTest { @Test void enrolCourse() throws Exception { - String id = "0"; - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", + Long id = 0L; + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", "Nováková", new AddressDto()); - CourseDto courseDtoWithStudent = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + CourseDto courseDtoWithStudent = new CourseDto(id, "english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); courseDtoWithStudent.setStudentIds(new ArrayList<>(List.of(student.getId()))); Mockito.when(courseController.enrol(ArgumentMatchers.eq(id), @@ -244,16 +240,16 @@ public class CourseTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) -// .andExpect(jsonPath("$.studentIds").value(student.getId())) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) + .andExpect(jsonPath("$.studentIds").exists()) .andReturn().getResponse().getContentAsString(); } @Test void enrolCourseWithoutUserParameter() throws Exception { String id = "0"; - Mockito.when(courseController.enrol(ArgumentMatchers.anyString(), + Mockito.when(courseController.enrol(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); mockMvc.perform(patch("/courses/enrol/" + id)) @@ -262,26 +258,26 @@ public class CourseTest { @Test void enrolCourseWithoutCourseIdParameter() throws Exception { - Mockito.when(courseController.enrol(ArgumentMatchers.anyString(), + Mockito.when(courseController.enrol(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", "Nováková", new AddressDto()); mockMvc.perform(patch("/courses/enrol/") - .content(asJsonString(student)) - .contentType(MediaType.APPLICATION_JSON)) + .content(asJsonString(student)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); } @Test void expelCourse() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(courseController.expel(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", "Nováková", new AddressDto()); mockMvc.perform(patch("/courses/expel/" + id) @@ -290,15 +286,15 @@ public class CourseTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) .andExpect(jsonPath("$.studentIds").isEmpty()) .andReturn().getResponse().getContentAsString(); } @Test void expelCourseWithoutUserParameter() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(courseController.expel(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); @@ -309,10 +305,10 @@ public class CourseTest { @Test void deleteCourseWithoutCourseIdParameter() throws Exception { - Mockito.when(courseController.expel(ArgumentMatchers.anyString(), + Mockito.when(courseController.expel(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", "Nováková", new AddressDto()); mockMvc.perform(patch("/courses/expel/") @@ -320,12 +316,4 @@ public class CourseTest { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); } - - public static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } } diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java index e254fd91b028d5636361c47afd12a0c5483ac2ab..a0d327ac0bd7511e93297ff5de1a6f77ab54971e 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java @@ -38,17 +38,17 @@ public class LectureTest { LocalDateTime.now().plusDays(2), LocalDateTime.now().plusDays(2).plusHours(2), "Learning how to spell deprecated", - 10, "0"); + 10, 0L); private final LectureDto lectureDto = new LectureDto( LocalDateTime.now().plusDays(2), LocalDateTime.now().plusDays(2).plusHours(2), "Learning how to spell deprecated", - 10, "0", "0"); + 10, 0L, 0L); @Test void createLecture() throws Exception { Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create") + mockMvc.perform(post("/lectures") .content(asJsonString(lectureCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -66,7 +66,7 @@ public class LectureTest { LectureCreateDto invalidLectureCreateDto = new LectureCreateDto(null, null, null, null, null); Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create") + mockMvc.perform(post("/lectures") .content(asJsonString(invalidLectureCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); @@ -75,13 +75,13 @@ public class LectureTest { @Test void createLectureWithoutParameter() throws Exception { Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create")) + mockMvc.perform(post("/lectures")) .andExpect(status().is4xxClientError()); } @Test void findLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.find(id)).thenReturn(lectureDto); mockMvc.perform(get("/lectures/find/" + id)) .andExpect(status().isOk()) @@ -96,17 +96,17 @@ public class LectureTest { @Test void findLectureWithoutId() throws Exception { - Mockito.when(lectureController.find(ArgumentMatchers.anyString())).thenReturn(lectureDto); + Mockito.when(lectureController.find(ArgumentMatchers.anyLong())).thenReturn(lectureDto); mockMvc.perform(get("/lectures/find/")) .andExpect(status().is4xxClientError()); } @Test void findLecturesByCourse() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.findByCourse(id)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/lectures/findByCourse") - .param("courseId", id)) + .param("courseId", String.valueOf(id))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); @@ -115,14 +115,14 @@ public class LectureTest { @Test void findLecturesByCourseWithoutParameter() throws Exception { - Mockito.when(lectureController.findByCourse(ArgumentMatchers.anyString())).thenReturn(new ArrayList<>()); + Mockito.when(lectureController.findByCourse(ArgumentMatchers.anyLong())).thenReturn(new ArrayList<>()); mockMvc.perform(get("/lectures/findByCourse")) .andExpect(status().is4xxClientError()); } @Test void updateLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.update(ArgumentMatchers.eq(id), ArgumentMatchers.isA(LectureCreateDto.class))) .thenReturn(lectureDto); @@ -142,7 +142,7 @@ public class LectureTest { @Test void updateLectureWithoutParameter() throws Exception { Mockito.when(lectureController. - update(ArgumentMatchers.anyString(), ArgumentMatchers.isA(LectureCreateDto.class))) + update(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(LectureCreateDto.class))) .thenReturn(lectureDto); mockMvc.perform(put("/lectures/update")) .andExpect(status().is4xxClientError()); @@ -150,7 +150,7 @@ public class LectureTest { @Test void deleteLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.doNothing().when(lectureController).delete(id); mockMvc.perform(delete("/lectures/delete/" + id)) .andExpect(status().isOk()); @@ -158,14 +158,14 @@ public class LectureTest { @Test void deleteCourseWithoutParameter() throws Exception { - Mockito.doNothing().when(lectureController).delete(ArgumentMatchers.anyString()); + Mockito.doNothing().when(lectureController).delete(ArgumentMatchers.anyLong()); mockMvc.perform(delete("/lectures/delete/")) .andExpect(status().is4xxClientError()); } @Test void setLecturerForLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.setLecturer(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); @@ -186,7 +186,7 @@ public class LectureTest { @Test void setLecturerForLectureWithoutParameters() throws Exception { - Mockito.when(lectureController.setLecturer(ArgumentMatchers.anyString(), + Mockito.when(lectureController.setLecturer(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/setLecturer")) @@ -195,7 +195,7 @@ public class LectureTest { @Test void enrolLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.enrol(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); @@ -216,7 +216,7 @@ public class LectureTest { @Test void enrolCourseWithoutUserParameters() throws Exception { - Mockito.when(lectureController.enrol(ArgumentMatchers.anyString(), + Mockito.when(lectureController.enrol(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/enrol")) @@ -225,7 +225,7 @@ public class LectureTest { @Test void expelLecture() throws Exception { - String id = "0"; + Long id = 0L; Mockito.when(lectureController.expel(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); @@ -246,7 +246,7 @@ public class LectureTest { @Test void expelCourseWithoutUserParameters() throws Exception { - Mockito.when(lectureController.expel(ArgumentMatchers.anyString(), + Mockito.when(lectureController.expel(ArgumentMatchers.anyLong(), ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/expel")) diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java index 8c71b4d614ebc330cade38a7abbe7fd1034c4b77..59f5032bf8a1fe520fa1160415230843c3d2850f 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java @@ -121,7 +121,7 @@ class UserControllerTest { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - long id = objectMapper.readValue(response, UserDto.class).getId(); + Long id = objectMapper.readValue(response, UserDto.class).getId(); mockMvc.perform(get("/users/{id}", id)) .andExpect(status().isOk()) @@ -142,7 +142,7 @@ class UserControllerTest { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - long id = objectMapper.readValue(response, UserDto.class).getId(); + Long id = objectMapper.readValue(response, UserDto.class).getId(); mockMvc.perform(delete("/users/{id}", id) .contentType(MediaType.APPLICATION_JSON)) @@ -156,7 +156,7 @@ class UserControllerTest { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - long id = objectMapper.readValue(response, UserDto.class).getId(); + Long id = objectMapper.readValue(response, UserDto.class).getId(); var updatedUsername = "novak"; var userToUpdate = new UserCreateDto( @@ -195,7 +195,7 @@ class UserControllerTest { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - long id = objectMapper.readValue(response, UserDto.class).getId(); + Long id = objectMapper.readValue(response, UserDto.class).getId(); mockMvc.perform(post("/users/logout/{id}", id)) .andExpect(status().isOk()); @@ -203,19 +203,19 @@ class UserControllerTest { @Test void getFinished() throws Exception { - mockMvc.perform(get("/users/finished/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af")) + mockMvc.perform(get("/users/finished/1", "1c1bbf66-6585-4978-886b-b126335ff3af")) .andExpect(status().isOk()); } @Test void getEnrolled() throws Exception { - mockMvc.perform(get("/users/enrolled/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af")) + mockMvc.perform(get("/users/enrolled/1", "1c1bbf66-6585-4978-886b-b126335ff3af")) .andExpect(status().isOk()); } @Test void addLanguage() throws Exception { - mockMvc.perform(put("/users/addLanguage/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af") + mockMvc.perform(put("/users/addLanguage/1", "1c1bbf66-6585-4978-886b-b126335ff3af") .content(asJsonString(new UserAddLanguageDto(LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2))) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); diff --git a/application/module-language-school/src/test/resources/application-test.properties b/application/module-language-school/src/test/resources/application-test.properties new file mode 100644 index 0000000000000000000000000000000000000000..896213ef130ffc73d291c5d76336ccf6aaef4f9f --- /dev/null +++ b/application/module-language-school/src/test/resources/application-test.properties @@ -0,0 +1,14 @@ +# For description of each field check: https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# INIT=CREATE SCHEMA IF NOT EXISTS PA165;SET SCHEMA PA165 +spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=SedaQ-app-test +spring.datasource.password= +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.properties.hibernate.generate_statistics=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=false +spring.h2.console.enabled=true +spring.cache.type=NONE +appconfig.enablecache=false \ No newline at end of file diff --git a/application/module-language-school/src/test/resources/logback.xml b/application/module-language-school/src/test/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..e449f9cd302a0db94a3f6d8469468a7b277ca638 --- /dev/null +++ b/application/module-language-school/src/test/resources/logback.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d %5p %40.40c:%4L - %m%n</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="console" /> + </root> +<!-- TODO remove solution--> + <logger name="org.hibernate.SQL" level="DEBUG"/> + +</configuration> \ No newline at end of file diff --git a/application/module-mail/Dockerfile b/application/module-mail/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2e3cde273701942cb4330be21845cd681a7bb084 --- /dev/null +++ b/application/module-mail/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-mail-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/pipeline.yml b/application/pipeline.yml index 2b29b6dd76dd775858d9b52bc2f6c7d185c40b29..58a8f7ffe73ca37ef5263e501cb0ddd83b9c0cd2 100644 --- a/application/pipeline.yml +++ b/application/pipeline.yml @@ -1,7 +1,9 @@ # This file specifies GitLab CI/CD pipeline, see https://docs.gitlab.com/ee/ci/ stages: + - build - test + - deploy variables: # variable read by Maven for JVM options, see https://maven.apache.org/configure.html#maven_opts-environment-variable @@ -9,19 +11,64 @@ variables: # our own variable for repeated options MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version " -test: +# name of Docker image in which the script commands are executed +image: maven:latest + +cache: + paths: + - .m2/repository + +build: + stage: build + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS clean install -DskipTests' + +test-certificate: stage: test tags: - shared-fi - # name of Docker image in which the script commands are executed image: maven:latest - # script is a list of linux shell commands script: - # run maven build - 'cd "$CI_PROJECT_DIR/application"' - - 'mvn $MAVEN_CLI_OPTS clean install' - cache: - # caches maven repo between runs - paths: - - .m2/repository + - 'mvn $MAVEN_CLI_OPTS -am -pl module-certificate test' +test-exercise: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-exercise test' + +test-language-school: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-language-school test' + +test-mail: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-mail test' + +deploy: + when: manual + stage: deploy + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS install -DskipTests' + - 'docker-compose build --parallel' + - 'docker login -u $DOCKER_PRIVATE_LOGIN -p $DOCKER_PRIVATE_PASSWORD' + - 'docker-compose push' diff --git a/application/podman-compose.yml b/application/podman-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..41a6a0d3edb0fa3ffa8d88abb80419ffb7e96025 --- /dev/null +++ b/application/podman-compose.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + certificate: + build: ./module-certificate + container_name: certificate + image: xpokorn8/sprachschulsystem:certificate + ports: + - "5001:5001" + + exercise: + build: ./module-exercise + container_name: exercise + image: xpokorn8/sprachschulsystem:exercise + ports: + - "5002:5002" + + language-school: + build: ./module-language-school + container_name: language-school + image: xpokorn8/sprachschulsystem:language-school + ports: + - "5000:5000" + + mail: + build: ./module-mail + container_name: mail + image: xpokorn8/sprachschulsystem:mail + ports: + - "5003:5003" \ No newline at end of file