diff --git a/src/main/java/org/wikitolearn/midtier/course/client/CourseClient.java b/src/main/java/org/wikitolearn/midtier/course/client/CourseClient.java index a5ab28a..faf5499 100644 --- a/src/main/java/org/wikitolearn/midtier/course/client/CourseClient.java +++ b/src/main/java/org/wikitolearn/midtier/course/client/CourseClient.java @@ -1,145 +1,145 @@ package org.wikitolearn.midtier.course.client; import java.net.URI; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.wikitolearn.midtier.course.entity.Course; import org.wikitolearn.midtier.course.entity.EntityList; -import org.wikitolearn.midtier.course.entity.dto.UpdateCourseClientDto; +import org.wikitolearn.midtier.course.entity.dto.in.UpdateCourseClientDto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import lombok.extern.slf4j.Slf4j; @Component @Slf4j public class CourseClient { private final RestTemplate client; @Autowired private ModelMapper modelMapper; @Value("${application.clients.courses-backend}") private String baseUrl; @Autowired private ObjectMapper objectMapper; public CourseClient(RestTemplateBuilder restTemplateBuilder) { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); this.client = restTemplateBuilder.requestFactory(() -> requestFactory).build(); } @HystrixCommand(fallbackMethod = "defaultCourses") public EntityList findAll(MultiValueMap params) { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses") .queryParams(params) .build() .encode() .toUri(); return client .exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody(); } public Course find(String courseId, MultiValueMap params) { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses/" + courseId) .queryParams(params) .build() .encode() .toUri(); return client.getForObject(uri, Course.class); } public EntityList findByChapterId(String chapterId) throws JsonProcessingException { MultiValueMap query = new LinkedMultiValueMap<>(); ObjectNode whereJsonObject = objectMapper.getNodeFactory().objectNode().put("chapters._id", chapterId); query.add("where", objectMapper.writeValueAsString(whereJsonObject)); URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses") .queryParams(query) .build() .encode() .toUri(); return client .exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody(); } public EntityList getAllCourseVersions(String courseId, MultiValueMap params) { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses/" + courseId) .queryParams(params) .build() .encode() .toUri(); return client .exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody(); } public Course save(Course course) throws JsonProcessingException { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses") .build() .encode() .toUri(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity httpEntity = new HttpEntity (course.toSchemaCompliant(), headers); return client.postForObject(uri, httpEntity, Course.class); } public Course update(Course course) throws JsonProcessingException { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses/" + course.getId()) .build() .encode() .toUri(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setIfMatch(course.getEtag()); HttpEntity httpEntity = new HttpEntity (modelMapper.map(course, UpdateCourseClientDto.class), headers); return client.patchForObject(uri, httpEntity, Course.class); } public Course delete(Course course) throws JsonProcessingException { URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl + "/courses/" + course.getId()) .build() .encode() .toUri(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setIfMatch(course.getEtag()); HttpEntity httpEntity = new HttpEntity(headers); return client.exchange(uri, HttpMethod.DELETE, httpEntity, Course.class).getBody(); } private String defaultCourses() { return "Hello default!"; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/config/Swagger2Configuration.java b/src/main/java/org/wikitolearn/midtier/course/config/Swagger2Configuration.java index 1d2cb0d..5f80bfb 100644 --- a/src/main/java/org/wikitolearn/midtier/course/config/Swagger2Configuration.java +++ b/src/main/java/org/wikitolearn/midtier/course/config/Swagger2Configuration.java @@ -1,31 +1,32 @@ package org.wikitolearn.midtier.course.config; import java.util.Collections; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class Swagger2Configuration { @Bean public Docket api() { - return new Docket(DocumentationType.SWAGGER_2).enableUrlTemplating(true).select().apis(RequestHandlerSelectors.basePackage("org.wikitolearn")) - .paths(PathSelectors.any()).build().apiInfo(apiInfo()); + return new Docket(DocumentationType.SWAGGER_2).useDefaultResponseMessages(false).enableUrlTemplating(true).select() + .apis(RequestHandlerSelectors.basePackage("org.wikitolearn")).paths(PathSelectors.any()).build() + .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfo("Course MidTier API", "", "v1", "", new Contact("WikiToLearn", "https://www.wikitolearn.org", "info@wikitolearn.org"), "AGPLv3+", "https://www.gnu.org/licenses/agpl-3.0.en.html", Collections.emptyList()); } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/AddCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java similarity index 95% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/AddCourseChaptersDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java index 8e9b08f..787e933 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/AddCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java @@ -1,41 +1,39 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - -import io.swagger.annotations.ApiModelProperty; - import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class AddCourseChaptersDto { private String title; private String language; private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInAddCourseChapters { @JsonProperty("_id") private String id; @JsonProperty("_version") private int version; @ApiModelProperty(value = "This property is needed only for the new chapter", required = false) private String title; @ApiModelProperty(value = "This property is needed only for the new chapter", required = false) private String language; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseDto.java similarity index 97% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCourseDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseDto.java index 485fb1f..f545423 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseDto.java @@ -1,77 +1,77 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.Date; import java.util.List; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class GetCourseDto { @JsonProperty("_id") private String id; @JsonProperty("_etag") private String etag; @JsonProperty("_version") private int version; @JsonProperty("_latest_version") private int latestVersion; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_updated") private Date updated; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_created") private Date created; private String title; private List authors; private String language; private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInCourse { @JsonProperty("_id") private String id; @JsonProperty("_version") private int version; private String title; private List pages; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class PageInChapter { @JsonProperty("_id") private String id; @JsonProperty("_version") private int version; private String title; } } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseVersionsDto.java similarity index 86% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseVersionsDto.java index 281d6b1..0675d07 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCourseVersionsDto.java @@ -1,65 +1,62 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.Date; import java.util.List; import java.util.Map; import org.wikitolearn.midtier.course.entity.Link; import org.wikitolearn.midtier.course.entity.Meta; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) -public class GetCoursesDto { +public class GetCourseVersionsDto { @JsonProperty("_items") - List items; + List items; @JsonProperty("_meta") Meta meta; @JsonProperty("_links") Map links; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) - public static final class CourseInListDto { + public static final class CourseVersionInListDto { @JsonProperty("_id") private String id; @JsonProperty("_etag") private String etag; @JsonProperty("_version") private int version; - @JsonProperty("_latest_version") - private int latestVersion; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_updated") private Date updated; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_created") private Date created; private String title; private List authors; private String language; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCoursesDto.java similarity index 96% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCoursesDto.java index 281d6b1..b52a202 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/GetCoursesDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/GetCoursesDto.java @@ -1,65 +1,65 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.Date; import java.util.List; import java.util.Map; import org.wikitolearn.midtier.course.entity.Link; import org.wikitolearn.midtier.course.entity.Meta; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class GetCoursesDto { @JsonProperty("_items") List items; @JsonProperty("_meta") Meta meta; @JsonProperty("_links") Map links; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class CourseInListDto { @JsonProperty("_id") private String id; @JsonProperty("_etag") private String etag; @JsonProperty("_version") private int version; @JsonProperty("_latest_version") private int latestVersion; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_updated") private Date updated; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_created") private Date created; private String title; private List authors; private String language; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreCourseDto.java new file mode 100644 index 0000000..6d0859c --- /dev/null +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreCourseDto.java @@ -0,0 +1,49 @@ +package org.wikitolearn.midtier.course.entity.dto.in; + +import java.util.List; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_DEFAULT) +public class StoreCourseDto { + @NotNull + private String title; + @NotNull + private String language; + private List chapters; + + @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(Include.NON_DEFAULT) + public static final class ChapterInCourse { + @NotNull + private String title; + @NotNull + private String language; + private List pages; + + @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(Include.NON_DEFAULT) + public static final class PageInChapter { + @NotNull + private String title; + @NotNull + private String language; + @NotNull + private String content; + } + } +} diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java similarity index 94% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseChaptersDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java index d931ae0..e961bf6 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java @@ -1,34 +1,34 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class UpdateCourseChaptersDto { private String title; private String language; private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInUpdateCourseChapters { @JsonProperty("_id") private String id; @JsonProperty("_version") private int version; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseClientDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java similarity index 93% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseClientDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java index babe959..b5f9905 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseClientDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java @@ -1,34 +1,34 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class UpdateCourseClientDto { private String title; private String language; private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInUpdateCourseChapter { @JsonProperty("_id") private String id; @JsonProperty("_version") private int version; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java similarity index 88% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java index db17b7b..664f5da 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdateCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java @@ -1,17 +1,17 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.in; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class UpdateCourseDto { private String title; private String language; } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedCourseChaptersDto.java similarity index 67% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedCourseChaptersDto.java index fb69c6d..0050e43 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedCourseChaptersDto.java @@ -1,38 +1,54 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.out; import java.util.Date; +import java.util.List; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) -public class UpdatedCourseDto { +public class AddedCourseChaptersDto { + @JsonProperty("_id") private String id; @JsonProperty("_etag") private String etag; @JsonProperty("_version") private int version; @JsonProperty("_latest_version") private int latestVersion; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_updated") private Date updated; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_created") private Date created; + + private List chapters; + + @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(Include.NON_DEFAULT) + public static final class ChapterInAddedCourseChapters { + @JsonProperty("_id") + private String id; + + @JsonProperty("_version") + private int version; + } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/StoredOrUpdatedCourseDto.java similarity index 90% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/out/StoredOrUpdatedCourseDto.java index fb69c6d..35a222d 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/UpdatedCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/StoredOrUpdatedCourseDto.java @@ -1,38 +1,38 @@ -package org.wikitolearn.midtier.course.entity.dto; +package org.wikitolearn.midtier.course.entity.dto.out; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) -public class UpdatedCourseDto { +public class StoredOrUpdatedCourseDto { @JsonProperty("_id") private String id; @JsonProperty("_etag") private String etag; @JsonProperty("_version") private int version; @JsonProperty("_latest_version") private int latestVersion; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_updated") private Date updated; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE, dd MMM yyyy HH:mm:ss z") @JsonProperty("_created") private Date created; } diff --git a/src/main/java/org/wikitolearn/midtier/course/service/CourseService.java b/src/main/java/org/wikitolearn/midtier/course/service/CourseService.java index 153fd04..5ba5172 100644 --- a/src/main/java/org/wikitolearn/midtier/course/service/CourseService.java +++ b/src/main/java/org/wikitolearn/midtier/course/service/CourseService.java @@ -1,145 +1,147 @@ package org.wikitolearn.midtier.course.service; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.util.MultiValueMap; import org.wikitolearn.midtier.course.client.CourseClient; import org.wikitolearn.midtier.course.entity.Chapter; import org.wikitolearn.midtier.course.entity.Course; import org.wikitolearn.midtier.course.entity.EntityList; import org.wikitolearn.midtier.course.event.ChapterDeleted; import org.wikitolearn.midtier.course.event.ChapterUpdated; import org.wikitolearn.midtier.course.exception.InvalidResourceCreateException; import org.wikitolearn.midtier.course.exception.InvalidResourceUpdateException; import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; @Service @Slf4j public class CourseService { @Autowired private CourseClient courseClient; @Autowired private ChapterService chapterService; public EntityList findAll(MultiValueMap params) { return courseClient.findAll(params); } public Course find(String courseId, MultiValueMap params) { return courseClient.find(courseId, params); } public Course findByChapterId(String chapterId) throws JsonProcessingException { EntityList courses = courseClient.findByChapterId(chapterId); return courses.getItems().get(0); } public EntityList getAllCourseVersions(String courseId, MultiValueMap params) { return courseClient.getAllCourseVersions(courseId, params); } public Course save(Course course) throws JsonProcessingException { if(course.getChapters()!= null && !course.getChapters().isEmpty()) { course.setChapters(chapterService.save(course.getChapters())); } return courseClient.save(course); } public Course update(Course course) throws JsonProcessingException { return courseClient.update(course); } public Course delete(Course course) throws JsonProcessingException { course = this.find(course.getId(), null); course.getChapters().stream().forEachOrdered(c -> { try { this.chapterService.delete(c, true); } catch (JsonProcessingException e) { // FIXME log.error(e.getMessage()); } }); return courseClient.delete(course); } public Course updateChapters(Course course) throws JsonProcessingException, InvalidResourceCreateException { List currentChapters = this.find(course.getId(), null).getChapters(); if(currentChapters.size() == course.getChapters().size() && currentChapters.containsAll(course.getChapters())) { return courseClient.update(course); } else { log.warn("Invalid course's chapters update request"); throw new InvalidResourceUpdateException("Invalid course's chapters update request"); } } public Course addChapters(Course course) throws JsonProcessingException, InvalidResourceCreateException { List currentChapters = this.find(course.getId(), null).getChapters(); List chaptersToAdd = new ArrayList<>(CollectionUtils.disjunction(course.getChapters(), currentChapters)); if(chaptersToAdd.size() != 1) { log.warn("Invalid course's chapter create request"); throw new InvalidResourceCreateException("Invalid course's chapter create request"); } Chapter addedChapter = chapterService.save(chaptersToAdd.get(0)); course.getChapters() .parallelStream() .filter(c -> chaptersToAdd.get(0).getTitle().equals(c.getTitle())) .findFirst() .map(c -> { c.setVersion(addedChapter.getLatestVersion()); c.setId(addedChapter.getId()); return c; }); - return this.update(course); + Course updatedCourse = this.update(course); + updatedCourse.setChapters(course.getChapters()); + return updatedCourse; } @EventListener public void handleChapterUpdatedEvent(ChapterUpdated event) throws JsonProcessingException { Chapter updatedChapter = event.getChapter(); Course course = this.findByChapterId(updatedChapter.getId()); course.getChapters().stream().forEach(c -> { if(c.getId().equals(updatedChapter.getId())) { c.setVersion(updatedChapter.getLatestVersion()); } }); this.update(course); } @EventListener public void handleChapterDeletedEvent(ChapterDeleted event) throws JsonProcessingException { Chapter deletedChapter = event.getChapter(); Course course = this.findByChapterId(deletedChapter.getId()); List chapters = course.getChapters() .stream() .sequential() .filter(removeDeletedChapter(deletedChapter.getId())) .collect(Collectors.toList()); course.setChapters(chapters); this.update(course); } private static Predicate removeDeletedChapter(String deletedChapterId) { return c -> !deletedChapterId.equals(c.getId()); } } diff --git a/src/main/java/org/wikitolearn/midtier/course/web/rest/CourseController.java b/src/main/java/org/wikitolearn/midtier/course/web/rest/CourseController.java index 2349d61..d976a30 100644 --- a/src/main/java/org/wikitolearn/midtier/course/web/rest/CourseController.java +++ b/src/main/java/org/wikitolearn/midtier/course/web/rest/CourseController.java @@ -1,148 +1,196 @@ package org.wikitolearn.midtier.course.web.rest; import java.util.Collections; import java.util.Optional; import java.util.stream.Collectors; +import javax.validation.Valid; + import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.wikitolearn.midtier.course.entity.Course; import org.wikitolearn.midtier.course.entity.EntityList; -import org.wikitolearn.midtier.course.entity.dto.AddCourseChaptersDto; -import org.wikitolearn.midtier.course.entity.dto.GetCourseDto; -import org.wikitolearn.midtier.course.entity.dto.GetCoursesDto; -import org.wikitolearn.midtier.course.entity.dto.UpdateCourseChaptersDto; -import org.wikitolearn.midtier.course.entity.dto.UpdateCourseDto; -import org.wikitolearn.midtier.course.entity.dto.UpdatedCourseDto; +import org.wikitolearn.midtier.course.entity.ErrorJson; +import org.wikitolearn.midtier.course.entity.dto.in.AddCourseChaptersDto; +import org.wikitolearn.midtier.course.entity.dto.in.GetCourseDto; +import org.wikitolearn.midtier.course.entity.dto.in.GetCoursesDto; +import org.wikitolearn.midtier.course.entity.dto.in.StoreCourseDto; +import org.wikitolearn.midtier.course.entity.dto.in.UpdateCourseChaptersDto; +import org.wikitolearn.midtier.course.entity.dto.in.UpdateCourseDto; +import org.wikitolearn.midtier.course.entity.dto.out.AddedCourseChaptersDto; +import org.wikitolearn.midtier.course.entity.dto.out.StoredOrUpdatedCourseDto; import org.wikitolearn.midtier.course.exception.InvalidResourceCreateException; import org.wikitolearn.midtier.course.service.ChapterService; import org.wikitolearn.midtier.course.service.CourseService; import org.wikitolearn.midtier.course.service.PageService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; @RestController @Slf4j @RequestMapping(value = "/courses") public class CourseController { @Autowired private ModelMapper modelMapper; @Autowired private CourseService courseService; @Autowired private ChapterService chapterService; @Autowired private PageService pageService; @Autowired private ObjectMapper objectMapper; - + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = GetCoursesDto.class) + }) @GetMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public GetCoursesDto getCourses(@RequestParam(value="page", required=false) Integer page) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("page", String.valueOf(page)); EntityList courses = courseService.findAll(params); return modelMapper.map(courses, GetCoursesDto.class); } - + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = GetCourseDto.class) + }) @GetMapping(value = "/{courseId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public GetCourseDto getCourse(@PathVariable String courseId) throws JsonProcessingException { MultiValueMap chapterParams = new LinkedMultiValueMap<>(); ObjectNode projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1).put("pages", 1); chapterParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); MultiValueMap pagesParams = new LinkedMultiValueMap<>(); projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1); pagesParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); Course course = courseService.find(courseId, null); course.setChapters( Optional .ofNullable(course.getChapters()) .orElseGet(Collections::emptyList) .parallelStream() .map(chapter -> { chapter = chapterService.find(chapter.getId(), chapterParams); chapter.setPages( Optional .ofNullable(chapter.getPages()) .orElseGet(Collections::emptyList) .parallelStream() .map(page -> pageService.find(page.getId(), pagesParams)) .collect(Collectors.toList())); return chapter; }) .collect(Collectors.toList())); return modelMapper.map(course, GetCourseDto.class); } - + + @ApiResponses({ + @ApiResponse(code = 201, message = "Created", response = StoredOrUpdatedCourseDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class), + @ApiResponse(code = 422, message = "Unprocessable Entity") + }) @PostMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public ResponseEntity storeCourse(@RequestBody Course course) throws JsonProcessingException { - ResponseEntity response = new ResponseEntity<>(courseService.save(course), HttpStatus.CREATED); - return response; + @ResponseStatus(HttpStatus.CREATED) + public StoredOrUpdatedCourseDto storeCourse(@Valid @RequestBody StoreCourseDto course) throws JsonProcessingException { + Course storedCourse = courseService.save(modelMapper.map(course, Course.class)); + return modelMapper.map(storedCourse, StoredOrUpdatedCourseDto.class); } - + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = StoredOrUpdatedCourseDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class), + @ApiResponse(code = 422, message = "Unprocessable Entity") + }) @PatchMapping(value = "/{courseId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public UpdatedCourseDto updateCourse(@PathVariable("courseId") String courseId, @RequestBody UpdateCourseDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + public StoredOrUpdatedCourseDto updateCourse(@PathVariable("courseId") String courseId, @RequestBody UpdateCourseDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException { Course courseToUpdate = modelMapper.map(course, Course.class); courseToUpdate.setId(courseId); courseToUpdate.setEtag(etag); Course updatedCourse = courseService.update(courseToUpdate); - return modelMapper.map(updatedCourse, UpdatedCourseDto.class); + return modelMapper.map(updatedCourse, StoredOrUpdatedCourseDto.class); } - + + @ApiResponses({ + @ApiResponse(code = 204, message = "No Content"), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class) + }) @DeleteMapping(value = "/{courseId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Course deleteCourse(@PathVariable("courseId") String courseId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCourse(@PathVariable("courseId") String courseId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { Course course = new Course(); course.setId(courseId); course.setEtag(etag); - - return courseService.delete(course); + courseService.delete(course); } - + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = StoredOrUpdatedCourseDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class), + @ApiResponse(code = 422, message = "Unprocessable Entity") + }) @PatchMapping(value = "/{courseId}/chapters", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public UpdatedCourseDto updateChapters(@PathVariable("courseId") String courseId, @RequestBody UpdateCourseChaptersDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { + public StoredOrUpdatedCourseDto updateChapters(@PathVariable("courseId") String courseId, @RequestBody UpdateCourseChaptersDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { Course courseToUpdate = modelMapper.map(course, Course.class); courseToUpdate.setId(courseId); courseToUpdate.setEtag(etag); Course updatedCourse = courseService.updateChapters(courseToUpdate); - return modelMapper.map(updatedCourse, UpdatedCourseDto.class); + return modelMapper.map(updatedCourse, StoredOrUpdatedCourseDto.class); } - + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = AddedCourseChaptersDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class), + @ApiResponse(code = 422, message = "Unprocessable Entity") + }) @PostMapping(value = "/{courseId}/chapters", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public UpdatedCourseDto addChapters(@PathVariable("courseId") String courseId, @RequestBody AddCourseChaptersDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { + public AddedCourseChaptersDto addChapters(@PathVariable("courseId") String courseId, @RequestBody AddCourseChaptersDto course, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { Course courseToUpdate = modelMapper.map(course, Course.class); courseToUpdate.setId(courseId); courseToUpdate.setEtag(etag); Course updatedCourse = courseService.addChapters(courseToUpdate); - return modelMapper.map(updatedCourse, UpdatedCourseDto.class); + return modelMapper.map(updatedCourse, AddedCourseChaptersDto.class); } } diff --git a/src/main/java/org/wikitolearn/midtier/course/web/rest/HistoryController.java b/src/main/java/org/wikitolearn/midtier/course/web/rest/HistoryController.java index 3e38fd5..002a27c 100644 --- a/src/main/java/org/wikitolearn/midtier/course/web/rest/HistoryController.java +++ b/src/main/java/org/wikitolearn/midtier/course/web/rest/HistoryController.java @@ -1,150 +1,156 @@ package org.wikitolearn.midtier.course.web.rest; import java.util.Collections; import java.util.Optional; import java.util.stream.Collectors; +import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.wikitolearn.midtier.course.entity.Chapter; import org.wikitolearn.midtier.course.entity.Course; import org.wikitolearn.midtier.course.entity.EntityList; import org.wikitolearn.midtier.course.entity.Page; +import org.wikitolearn.midtier.course.entity.dto.in.GetCourseDto; +import org.wikitolearn.midtier.course.entity.dto.in.GetCourseVersionsDto; import org.wikitolearn.midtier.course.exception.ResourceNotFoundException; import org.wikitolearn.midtier.course.service.ChapterService; import org.wikitolearn.midtier.course.service.CourseService; import org.wikitolearn.midtier.course.service.PageService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @RestController @Slf4j @RequestMapping(value = "/history/courses") public class HistoryController { @Autowired private CourseService courseService; @Autowired private ChapterService chapterService; @Autowired private PageService pageService; @Autowired private ObjectMapper objectMapper; + @Autowired + private ModelMapper modelMapper; + @ApiOperation(value = "getCourseVersion") @GetMapping(value = "/{courseId}/versions/{version}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Course getCourseVersion(@PathVariable(value = "courseId", required = true) String courseId, + public GetCourseDto getCourseVersion(@PathVariable(value = "courseId", required = true) String courseId, @PathVariable(value = "version", required = true) Integer version) throws JsonProcessingException { MultiValueMap courseParams = new LinkedMultiValueMap<>(); courseParams.add("version", String.valueOf(version)); MultiValueMap chapterParams = new LinkedMultiValueMap<>(); ObjectNode projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1).put("pages", 1); chapterParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); chapterParams.add("version", ""); MultiValueMap pagesParams = new LinkedMultiValueMap<>(); projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1); pagesParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); pagesParams.add("version", ""); Course course = courseService.find(courseId, courseParams); course.setChapters( Optional .ofNullable(course.getChapters()) .orElseGet(Collections::emptyList) .parallelStream() .map(chapter -> { chapterParams.set("version", String.valueOf(chapter.getVersion())); chapter = chapterService.find(chapter.getId(), chapterParams); chapter.setPages( Optional .ofNullable(chapter.getPages()) .orElseGet(Collections::emptyList) .parallelStream() .map(page -> { pagesParams.set("version", String.valueOf(page.getVersion())); return pageService.find(page.getId(), pagesParams); }) .collect(Collectors.toList())); return chapter; }) .collect(Collectors.toList())); - return course; + return modelMapper.map(course, GetCourseDto.class); } @GetMapping(value = "/{courseId}/versions", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public EntityList getCourseVersions(@PathVariable(value = "courseId", required = true) String courseId, + public GetCourseVersionsDto getCourseVersions(@PathVariable(value = "courseId", required = true) String courseId, @RequestParam(value="page", required=false) Integer page) { MultiValueMap courseParams = new LinkedMultiValueMap<>(); courseParams.add("page", String.valueOf(page)); courseParams.add("version", "all"); EntityList courseVersions = courseService.getAllCourseVersions(courseId, courseParams); - return courseVersions; + return modelMapper.map(courseVersions, GetCourseVersionsDto.class); } @GetMapping(value = "/{courseId}/versions/{version}", params = "pageId", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Page getPage(@PathVariable(value = "courseId", required = true) String courseId, @PathVariable(value = "version", required = true) Integer version, @RequestParam(value = "pageId", required = true) String pageId) throws JsonProcessingException { MultiValueMap courseParams = new LinkedMultiValueMap<>(); courseParams.add("version", String.valueOf(version)); MultiValueMap chapterParams = new LinkedMultiValueMap<>(); ObjectNode projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1).put("pages", 1); chapterParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); chapterParams.add("version", ""); MultiValueMap pageParams = new LinkedMultiValueMap<>(); pageParams.add("version", ""); Course course = courseService.find(courseId, courseParams); Chapter chapterTarget = Optional .ofNullable(course.getChapters()) .orElseGet(Collections::emptyList) .parallelStream() .filter(chapter -> { chapterParams.set("version", String.valueOf(chapter.getVersion())); Chapter enrichedChapter = chapterService.find(chapter.getId(), chapterParams); boolean isPagePresent = Optional.ofNullable(enrichedChapter.getPages()).orElseGet(Collections::emptyList) .parallelStream().filter(page -> pageId.equals(page.getId())).findFirst().isPresent(); if (!isPagePresent) { return false; } chapter.setPages(enrichedChapter.getPages()); return true; }) .findFirst() .orElseThrow(() -> new ResourceNotFoundException()); return Optional.ofNullable(chapterTarget.getPages()).orElseGet(Collections::emptyList).parallelStream() .filter(page -> pageId.equals(page.getId())) .findFirst() .map(page -> { pageParams.set("version", String.valueOf(page.getVersion())); return pageService.find(page.getId(), pageParams); }) .orElseThrow(() -> new ResourceNotFoundException()); } }