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 faf5499..5eea18c 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.in.UpdateCourseClientDto; +import org.wikitolearn.midtier.course.entity.dto.out.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/entity/dto/in/AddCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddChapterPagesDto.java similarity index 66% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddChapterPagesDto.java index 787e933..6e0a268 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddChapterPagesDto.java @@ -1,39 +1,49 @@ package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; +import javax.validation.constraints.NotEmpty; +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 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 { - +public class AddChapterPagesDto { + @NotNull private String title; + @NotNull private String language; - private List chapters; + @NotEmpty + @NotNull + private List pages; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) - public static final class ChapterInAddCourseChapters { + public static final class PageInAddChapterPages { @JsonProperty("_id") + @NotNull private String id; @JsonProperty("_version") + @NotNull private int version; - @ApiModelProperty(value = "This property is needed only for the new chapter", required = false) + @ApiModelProperty(value = "This property is needed only for the new page", required = false) private String title; - @ApiModelProperty(value = "This property is needed only for the new chapter", required = false) + @ApiModelProperty(value = "This property is needed only for the new page", required = false) private String language; + @ApiModelProperty(value = "This property is needed only for the new page", required = false) + private String content; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java index 787e933..c6a9046 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/AddCourseChaptersDto.java @@ -1,39 +1,47 @@ package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; +import javax.validation.constraints.NotEmpty; +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 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 { - + @NotNull private String title; + @NotNull private String language; + @NotEmpty + @NotNull private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInAddCourseChapters { @JsonProperty("_id") + @NotNull private String id; @JsonProperty("_version") + @NotNull 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/in/UpdateCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreOrUpdatePageDto.java similarity index 75% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreOrUpdatePageDto.java index 664f5da..892fcfa 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/StoreOrUpdatePageDto.java @@ -1,17 +1,23 @@ package org.wikitolearn.midtier.course.entity.dto.in; +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 UpdateCourseDto { +public class StoreOrUpdatePageDto { + @NotNull private String title; + @NotNull private String language; + @NotNull + private String content; } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterDto.java similarity index 81% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterDto.java index 664f5da..5716d05 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterDto.java @@ -1,17 +1,21 @@ package org.wikitolearn.midtier.course.entity.dto.in; +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 UpdateCourseDto { +public class UpdateChapterDto { + @NotNull private String title; + @NotNull private String language; } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterPagesDto.java similarity index 71% copy from src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java copy to src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterPagesDto.java index e961bf6..18119c8 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateChapterPagesDto.java @@ -1,34 +1,42 @@ package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; +import javax.validation.constraints.NotEmpty; +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 com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) -public class UpdateCourseChaptersDto { - +public class UpdateChapterPagesDto { + @NotNull private String title; + @NotNull private String language; - private List chapters; + @NotNull + @NotEmpty + private List pages; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) - public static final class ChapterInUpdateCourseChapters { + public static final class PageInUpdateChapterPages { @JsonProperty("_id") + @NotNull private String id; @JsonProperty("_version") + @NotNull private int version; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java index e961bf6..41835d0 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseChaptersDto.java @@ -1,34 +1,42 @@ package org.wikitolearn.midtier.course.entity.dto.in; import java.util.List; +import javax.validation.constraints.NotEmpty; +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 com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public class UpdateCourseChaptersDto { - + @NotNull private String title; + @NotNull private String language; + @NotNull + @NotEmpty private List chapters; @Data @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_DEFAULT) public static final class ChapterInUpdateCourseChapters { @JsonProperty("_id") + @NotNull private String id; @JsonProperty("_version") + @NotNull private int version; } } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java index 664f5da..8adc2f1 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseDto.java @@ -1,17 +1,21 @@ package org.wikitolearn.midtier.course.entity.dto.in; +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 UpdateCourseDto { + @NotNull private String title; + @NotNull private String language; } diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedChapterPagesDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedChapterPagesDto.java new file mode 100644 index 0000000..efceedd --- /dev/null +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/AddedChapterPagesDto.java @@ -0,0 +1,54 @@ +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 AddedChapterPagesDto { + + @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 pages; + + @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(Include.NON_DEFAULT) + public static final class PageInAddedChapterPages { + @JsonProperty("_id") + private String id; + + @JsonProperty("_version") + private int version; + } +} diff --git a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/UpdateCourseClientDto.java similarity index 93% rename from src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java rename to src/main/java/org/wikitolearn/midtier/course/entity/dto/out/UpdateCourseClientDto.java index b5f9905..da83e8b 100644 --- a/src/main/java/org/wikitolearn/midtier/course/entity/dto/in/UpdateCourseClientDto.java +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/UpdateCourseClientDto.java @@ -1,34 +1,34 @@ -package org.wikitolearn.midtier.course.entity.dto.in; +package org.wikitolearn.midtier.course.entity.dto.out; 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/out/UpdatedChapter.java b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/UpdatedChapter.java new file mode 100644 index 0000000..ab7a417 --- /dev/null +++ b/src/main/java/org/wikitolearn/midtier/course/entity/dto/out/UpdatedChapter.java @@ -0,0 +1,38 @@ +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 UpdatedChapter { + @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/ChapterService.java b/src/main/java/org/wikitolearn/midtier/course/service/ChapterService.java index f7db985..a62c935 100644 --- a/src/main/java/org/wikitolearn/midtier/course/service/ChapterService.java +++ b/src/main/java/org/wikitolearn/midtier/course/service/ChapterService.java @@ -1,171 +1,178 @@ package org.wikitolearn.midtier.course.service; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; 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.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.wikitolearn.midtier.course.client.ChapterClient; import org.wikitolearn.midtier.course.entity.Chapter; import org.wikitolearn.midtier.course.entity.EntityList; import org.wikitolearn.midtier.course.entity.Page; import org.wikitolearn.midtier.course.event.ChapterDeleted; import org.wikitolearn.midtier.course.event.ChapterUpdated; import org.wikitolearn.midtier.course.event.PageDeleted; import org.wikitolearn.midtier.course.event.PageUpdated; import org.wikitolearn.midtier.course.exception.InvalidResourceCreateException; import org.wikitolearn.midtier.course.exception.InvalidResourceUpdateException; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; @Service @Slf4j public class ChapterService { @Autowired private ApplicationEventPublisher applicationEventPublisher; @Autowired private ChapterClient chapterClient; @Autowired private PageService pageService; + + @Autowired + private ObjectMapper objectMapper; public EntityList findAll() { return chapterClient.findAll(); } public Chapter find(String chapterId, MultiValueMap params) { return chapterClient.find(chapterId, params); } public List save(List chapters) { chapters.stream().forEach(c -> { Chapter saved; try { saved = this.save(c); c.setId(saved.getId()); c.setVersion(saved.getVersion()); } catch (JsonProcessingException e) { log.error(e.getMessage()); } }); return chapters; } public Chapter findByPageId(String pageId) throws JsonProcessingException { EntityList chapters = chapterClient.findByPageId(pageId); return chapters.getItems().get(0); } public Chapter save(Chapter chapter) throws JsonProcessingException { if(chapter.getPages() != null && !chapter.getPages().isEmpty()) { chapter.setPages(pageService.save(chapter.getPages())); } return chapterClient.store(chapter); } public Chapter update(Chapter chapter) throws JsonProcessingException { Chapter updatedChapter = chapterClient.update(chapter); applicationEventPublisher.publishEvent(new ChapterUpdated(this, updatedChapter)); return updatedChapter; } public Chapter delete(Chapter chapter, boolean isBulkDelete) throws JsonProcessingException { MultiValueMap chapterParams = new LinkedMultiValueMap<>(); - chapterParams.add("projection", "{\"title\":1, \"pages\":1}"); + ObjectNode projectionJsonObject = objectMapper.getNodeFactory().objectNode().put("title", 1).put("pages", 1); + chapterParams.add("projection", objectMapper.writeValueAsString(projectionJsonObject)); + chapter = this.find(chapter.getId(), chapterParams); - log.info(chapter.toString()); + Optional .ofNullable(chapter.getPages()) .orElseGet(Collections::emptyList) .stream().forEachOrdered(p -> { try { pageService.delete(p, true); } catch (JsonProcessingException e) { // FIXME log.error(e.getMessage()); } }); Chapter deletedChapter = chapterClient.delete(chapter); if (!isBulkDelete) { applicationEventPublisher.publishEvent(new ChapterDeleted(this, chapter)); } return deletedChapter; } public Chapter updatePages(Chapter chapter) throws JsonProcessingException, InvalidResourceCreateException { List currentPages = this.find(chapter.getId(), null).getPages(); if(currentPages.size() == chapter.getPages().size() && currentPages.containsAll(chapter.getPages())) { return this.update(chapter); } else { log.warn("Invalid chapter's pages update request"); throw new InvalidResourceUpdateException("Invalid chapter's pages update request"); } } public Chapter addPages(Chapter chapter) throws JsonProcessingException, InvalidResourceCreateException { List currentPages = this.find(chapter.getId(), null).getPages(); List pagesToAdd = new ArrayList<>(CollectionUtils.disjunction(chapter.getPages(), currentPages)); if(pagesToAdd.size() != 1) { log.warn("Invalid chapter's pages create request"); throw new InvalidResourceCreateException("Invalid chapter's pages create request"); } Page addedPage = pageService.save(pagesToAdd.get(0)); chapter.getPages().stream().forEachOrdered(p -> { if(pagesToAdd.get(0).getTitle().equals(p.getTitle())) { p.setVersion(addedPage.getLatestVersion()); p.setId(addedPage.getId()); } }); return this.update(chapter); } @EventListener public void handlePageUpdatedEvent(PageUpdated event) throws JsonProcessingException { Page updatedPage = event.getPage(); Chapter chapter = this.findByPageId(updatedPage.getId()); chapter.getPages().stream().forEach(p -> { if(p.getId().equals(updatedPage.getId())) { p.setVersion(updatedPage.getLatestVersion()); } }); this.update(chapter); } @EventListener public void handlePageDeletedEvent(PageDeleted event) throws JsonProcessingException { Page deletedPage = event.getPage(); Chapter chapter = this.findByPageId(deletedPage.getId()); List pages = chapter.getPages() .stream() .sequential() .filter(removeDeletedPage(deletedPage.getId())) .collect(Collectors.toList()); chapter.setPages(pages); this.update(chapter); } private static Predicate removeDeletedPage(String deletedPageId) { return p -> !deletedPageId.equals(p.getId()); } } diff --git a/src/main/java/org/wikitolearn/midtier/course/web/rest/ChapterController.java b/src/main/java/org/wikitolearn/midtier/course/web/rest/ChapterController.java index 25f0744..4b01515 100644 --- a/src/main/java/org/wikitolearn/midtier/course/web/rest/ChapterController.java +++ b/src/main/java/org/wikitolearn/midtier/course/web/rest/ChapterController.java @@ -1,57 +1,111 @@ package org.wikitolearn.midtier.course.web.rest; +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.web.bind.annotation.DeleteMapping; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.wikitolearn.midtier.course.entity.Chapter; +import org.wikitolearn.midtier.course.entity.ErrorJson; +import org.wikitolearn.midtier.course.entity.dto.in.AddChapterPagesDto; +import org.wikitolearn.midtier.course.entity.dto.in.UpdateChapterDto; +import org.wikitolearn.midtier.course.entity.dto.in.UpdateChapterPagesDto; +import org.wikitolearn.midtier.course.entity.dto.out.AddedChapterPagesDto; +import org.wikitolearn.midtier.course.entity.dto.out.UpdatedChapter; import org.wikitolearn.midtier.course.exception.InvalidResourceCreateException; import org.wikitolearn.midtier.course.service.ChapterService; import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; @RestController @Slf4j @RequestMapping(value = "/chapters") public class ChapterController { @Autowired private ChapterService chapterService; + @Autowired + private ModelMapper modelMapper; + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = UpdatedChapter.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 422, message = "Uprocessable Entity"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class) + }) @PatchMapping(value = "/{chapterId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Chapter update(@PathVariable("chapterId") String chapterId, @RequestBody Chapter chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException { - chapter.setId(chapterId); - chapter.setEtag(etag); - return chapterService.update(chapter); + public UpdatedChapter update(@PathVariable("chapterId") String chapterId, @Valid @RequestBody UpdateChapterDto chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + Chapter chapterToUpdate = modelMapper.map(chapter, Chapter.class); + chapterToUpdate.setId(chapterId); + chapterToUpdate.setEtag(etag); + + Chapter updatedChapter = chapterService.update(chapterToUpdate); + + return modelMapper.map(updatedChapter, UpdatedChapter.class); } + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = UpdateChapterPagesDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 422, message = "Uprocessable Entity"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class) + }) @PatchMapping(value = "/{chapterId}/pages", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Chapter updatePages(@PathVariable("chapterId") String chapterId, @RequestBody Chapter chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { - chapter.setId(chapterId); - chapter.setEtag(etag); - return chapterService.updatePages(chapter); + public UpdateChapterPagesDto updatePages(@PathVariable("chapterId") String chapterId, @Valid @RequestBody UpdateChapterPagesDto chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { + Chapter chapterToUpdate = modelMapper.map(chapter, Chapter.class); + chapterToUpdate.setId(chapterId); + chapterToUpdate.setEtag(etag); + + Chapter updatedChapter = chapterService.updatePages(chapterToUpdate); + return modelMapper.map(updatedChapter, UpdateChapterPagesDto.class); } + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = AddedChapterPagesDto.class), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Forbidden"), + @ApiResponse(code = 422, message = "Uprocessable Entity"), + @ApiResponse(code = 500, message = "Internal Server Error", response = ErrorJson.class) + }) @PostMapping(value = "/{chapterId}/pages", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Chapter addPages(@PathVariable("chapterId") String chapterId, @RequestBody Chapter chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { - chapter.setId(chapterId); - chapter.setEtag(etag); - return chapterService.addPages(chapter); + public AddedChapterPagesDto addPages(@PathVariable("chapterId") String chapterId, @Valid @RequestBody AddChapterPagesDto chapter, @RequestHeader("If-Match") String etag) throws JsonProcessingException, InvalidResourceCreateException { + Chapter chapterToUpdate = modelMapper.map(chapter, Chapter.class); + chapterToUpdate.setId(chapterId); + chapterToUpdate.setEtag(etag); + + Chapter updatedChapter = chapterService.addPages(chapterToUpdate); + return modelMapper.map(updatedChapter, AddedChapterPagesDto.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 = "/{chapterId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Chapter delete(@PathVariable("chapterId") String chapterId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable("chapterId") String chapterId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { Chapter chapter = new Chapter(); chapter.setId(chapterId); chapter.setEtag(etag); - return chapterService.delete(chapter, false); + chapterService.delete(chapter, false); } } diff --git a/src/main/java/org/wikitolearn/midtier/course/web/rest/PageController.java b/src/main/java/org/wikitolearn/midtier/course/web/rest/PageController.java index f076383..b7eb038 100644 --- a/src/main/java/org/wikitolearn/midtier/course/web/rest/PageController.java +++ b/src/main/java/org/wikitolearn/midtier/course/web/rest/PageController.java @@ -1,47 +1,77 @@ package org.wikitolearn.midtier.course.web.rest; +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.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.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.wikitolearn.midtier.course.entity.ErrorJson; import org.wikitolearn.midtier.course.entity.Page; +import org.wikitolearn.midtier.course.entity.dto.in.StoreOrUpdatePageDto; import org.wikitolearn.midtier.course.service.PageService; import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; @RestController @Slf4j @RequestMapping(value = "/pages") public class PageController { @Autowired private PageService pageService; + @Autowired ModelMapper modelMapper; + + @ApiResponses({ + @ApiResponse(code = 200, message = "Success"), + }) @GetMapping(value = "/{pageId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Page getPage(@PathVariable String pageId) { return pageService.find(pageId, null); } + @ApiResponses({ + @ApiResponse(code = 200, message = "Success", response = Page.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 = "Uprocessable Entity") + }) @PatchMapping(value = "/{pageId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Page update(@PathVariable("pageId") String pageId, @RequestBody Page page, @RequestHeader("If-Match") String etag) throws JsonProcessingException { - page.setId(pageId); - page.setEtag(etag); - return pageService.update(page); + public Page update(@PathVariable("pageId") String pageId, @Valid @RequestBody StoreOrUpdatePageDto page, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + Page pageToUpdate = modelMapper.map(page, Page.class); + pageToUpdate.setId(pageId); + pageToUpdate.setEtag(etag); + + return pageService.update(pageToUpdate); } + @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 = "/{pageId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Page update(@PathVariable("pageId") String pageId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable("pageId") String pageId, @RequestHeader("If-Match") String etag) throws JsonProcessingException { Page page = new Page(); page.setId(pageId); page.setEtag(etag); - return pageService.delete(page, false); + pageService.delete(page, false); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 86211e5..f8220b8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,51 +1,49 @@ # Spring properties spring: application: name: CourseMidTier output: ansi: enabled: DETECT # Security properties security: oauth2: resource: user-info-uri: ${KEYCLOAK_URI}/auth/realms/${KEYCLOAK_AUTH_REALM}/protocol/openid-connect/userinfo token-info-uri: ${KEYCLOAK_URI}/auth/realms/${KEYCLOAK_AUTH_REALM}/protocol/openid-connect/token/introspect prefer-token-info: false -# jwt: -# key-uri: ${KEYCLOAK_URI}/auth/realms/${KEYCLOAK_AUTH_REALM} jwk: key-set-uri: ${KEYCLOAK_URI}/auth/realms/${KEYCLOAK_AUTH_REALM}/protocol/openid-connect/certs # Springfox properties springfox: documentation: swagger: v2: path: /api-docs # Logging properties logging: pattern: file: '%d{"yyyy-MM-dd HH:mm:ss,SSS"} %-5level [%c] \(%thread\) : %msg%n' console: '%d{"yyyy-MM-dd HH:mm:ss,SSS"} %-5level [%c] \(%thread\) : %msg%n' # Server properties server: port: ${SERVICE_PORT} ssl: enabled: ${SSL_ENABLED} key-store: file:./app-keystore.jks key-store-password: changeme key-password: changeme trust-store: file:./app-truststore.jks trust-store-password: changeme client-auth: ${MTLS_STATUS} # Application properties application: clients: courses-backend: ${COURSES_BACKEND_URI}/v1 chapters-backend: ${CHAPTERS_BACKEND_URI}/v1 pages-backend: ${PAGES_BACKEND_URI}/v1 \ No newline at end of file