From 3c93cb03b62c32b606fbfeeb4910437f741cb000 Mon Sep 17 00:00:00 2001 From: Kirill Mokevnin Date: Sat, 14 Oct 2023 22:43:30 +0600 Subject: [PATCH] add specification Signed-off-by: Kirill Mokevnin --- .../api/PostsCommentsController.java | 37 +++++++ .../blog/controller/api/PostsController.java | 11 ++- .../hexlet/blog/dto/PostCommentParamsDTO.java | 15 +++ src/main/java/io/hexlet/blog/model/Page.java | 3 +- src/main/java/io/hexlet/blog/model/Post.java | 4 +- .../io/hexlet/blog/model/PostComment.java | 6 +- .../repository/PostCommentRepository.java | 17 ++++ .../blog/repository/PostRepository.java | 3 +- .../io/hexlet/blog/service/PostService.java | 62 ++++++++++++ .../PostCommentSpecification.java | 30 ++++++ .../api/PostsCommentsControllerTest.java | 96 +++++++++++++++++++ .../io/hexlet/blog/util/ModelGenerator.java | 7 ++ 12 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/hexlet/blog/controller/api/PostsCommentsController.java create mode 100644 src/main/java/io/hexlet/blog/dto/PostCommentParamsDTO.java create mode 100644 src/main/java/io/hexlet/blog/repository/PostCommentRepository.java create mode 100644 src/main/java/io/hexlet/blog/service/PostService.java create mode 100644 src/main/java/io/hexlet/blog/specification/PostCommentSpecification.java create mode 100644 src/test/java/io/hexlet/blog/controller/api/PostsCommentsControllerTest.java diff --git a/src/main/java/io/hexlet/blog/controller/api/PostsCommentsController.java b/src/main/java/io/hexlet/blog/controller/api/PostsCommentsController.java new file mode 100644 index 0000000..fc18bab --- /dev/null +++ b/src/main/java/io/hexlet/blog/controller/api/PostsCommentsController.java @@ -0,0 +1,37 @@ +package io.hexlet.blog.controller.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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 io.hexlet.blog.dto.PostCommentParamsDTO; +import io.hexlet.blog.model.PostComment; +import io.hexlet.blog.repository.PostCommentRepository; +import io.hexlet.blog.specification.PostCommentSpecification; + +@RestController +@RequestMapping("/api") +public class PostsCommentsController { + @Autowired + private PostCommentRepository repository; + + @Autowired + private PostCommentSpecification specBuilder; + + @GetMapping("/posts_comments") + @ResponseStatus(HttpStatus.OK) + Page index(PostCommentParamsDTO params, @RequestParam(defaultValue = "1") int page) { + var spec = specBuilder.build(params); + var comments = repository.findAll(spec, PageRequest.of(page - 1, 10)); + + return comments; + } +} + diff --git a/src/main/java/io/hexlet/blog/controller/api/PostsController.java b/src/main/java/io/hexlet/blog/controller/api/PostsController.java index ff1dbdb..e7d34de 100644 --- a/src/main/java/io/hexlet/blog/controller/api/PostsController.java +++ b/src/main/java/io/hexlet/blog/controller/api/PostsController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import io.hexlet.blog.service.PostService; import io.hexlet.blog.dto.PostCreateDTO; import io.hexlet.blog.dto.PostDTO; import io.hexlet.blog.dto.PostUpdateDTO; @@ -36,17 +37,17 @@ public class PostsController { @Autowired private UserUtils userUtils; + @Autowired + private PostService postService; + @GetMapping("/posts") @ResponseStatus(HttpStatus.OK) ResponseEntity> index() { - var posts = repository.findAll(); - var result = posts.stream() - .map(postMapper::map) - .toList(); + var posts = postService.getAll(); return ResponseEntity.ok() .header("X-Total-Count", String.valueOf(posts.size())) - .body(result); + .body(posts); } @PostMapping("/posts") diff --git a/src/main/java/io/hexlet/blog/dto/PostCommentParamsDTO.java b/src/main/java/io/hexlet/blog/dto/PostCommentParamsDTO.java new file mode 100644 index 0000000..7350d86 --- /dev/null +++ b/src/main/java/io/hexlet/blog/dto/PostCommentParamsDTO.java @@ -0,0 +1,15 @@ +package io.hexlet.blog.dto; + +import java.util.Date; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class PostCommentParamsDTO { + private String nameCont; + private Long authorId; + private Long postId; + private Date createdAtGt; +} diff --git a/src/main/java/io/hexlet/blog/model/Page.java b/src/main/java/io/hexlet/blog/model/Page.java index 6ff20ec..5e84d05 100644 --- a/src/main/java/io/hexlet/blog/model/Page.java +++ b/src/main/java/io/hexlet/blog/model/Page.java @@ -1,10 +1,9 @@ package io.hexlet.blog.model; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -@NoArgsConstructor +// @NoArgsConstructor @Setter @Getter public class Page { diff --git a/src/main/java/io/hexlet/blog/model/Post.java b/src/main/java/io/hexlet/blog/model/Post.java index 0945072..61f6fb0 100644 --- a/src/main/java/io/hexlet/blog/model/Post.java +++ b/src/main/java/io/hexlet/blog/model/Post.java @@ -8,10 +8,11 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import com.fasterxml.jackson.annotation.JsonIgnore; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; @@ -37,6 +38,7 @@ public class Post implements BaseEntity { @EqualsAndHashCode.Include private Long id; + @JsonIgnore @ManyToOne(optional = false) // @NotNull private User author; diff --git a/src/main/java/io/hexlet/blog/model/PostComment.java b/src/main/java/io/hexlet/blog/model/PostComment.java index 36d45b5..20ca9c2 100644 --- a/src/main/java/io/hexlet/blog/model/PostComment.java +++ b/src/main/java/io/hexlet/blog/model/PostComment.java @@ -4,10 +4,11 @@ import java.util.Date; -import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; +import com.fasterxml.jackson.annotation.JsonIgnore; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -32,7 +33,8 @@ public class PostComment implements BaseEntity { @GeneratedValue(strategy = IDENTITY) private Long id; - @CreatedBy + @JsonIgnore + @ManyToOne(optional = false) private User author; @NotNull diff --git a/src/main/java/io/hexlet/blog/repository/PostCommentRepository.java b/src/main/java/io/hexlet/blog/repository/PostCommentRepository.java new file mode 100644 index 0000000..8329b5d --- /dev/null +++ b/src/main/java/io/hexlet/blog/repository/PostCommentRepository.java @@ -0,0 +1,17 @@ +package io.hexlet.blog.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import io.hexlet.blog.model.Post; +import io.hexlet.blog.model.PostComment; + +@Repository +public interface PostCommentRepository extends JpaRepository, JpaSpecificationExecutor { + // Page findAll(Specification spec, Pageable pageable); +} + diff --git a/src/main/java/io/hexlet/blog/repository/PostRepository.java b/src/main/java/io/hexlet/blog/repository/PostRepository.java index 0e81fe3..ad61abc 100644 --- a/src/main/java/io/hexlet/blog/repository/PostRepository.java +++ b/src/main/java/io/hexlet/blog/repository/PostRepository.java @@ -3,11 +3,12 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import io.hexlet.blog.model.Post; @Repository -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, JpaSpecificationExecutor { Optional findBySlug(String slug); } diff --git a/src/main/java/io/hexlet/blog/service/PostService.java b/src/main/java/io/hexlet/blog/service/PostService.java new file mode 100644 index 0000000..ab22d0b --- /dev/null +++ b/src/main/java/io/hexlet/blog/service/PostService.java @@ -0,0 +1,62 @@ +package io.hexlet.blog.service; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import io.hexlet.blog.dto.PostCreateDTO; +import io.hexlet.blog.dto.PostDTO; +import io.hexlet.blog.dto.PostUpdateDTO; +import io.hexlet.blog.exception.ResourceNotFoundException; +import io.hexlet.blog.mapper.PostMapper; +import io.hexlet.blog.repository.PostRepository; +import io.hexlet.blog.util.UserUtils; + +@Service +public class PostService { + @Autowired + private PostRepository repository; + + @Autowired + private PostMapper postMapper; + + @Autowired + private UserUtils userUtils; + + public List getAll() { + var posts = repository.findAll(); + var result = posts.stream() + .map(postMapper::map) + .toList(); + return result; + } + + PostDTO create(PostCreateDTO postData) { + var post = postMapper.map(postData); + post.setAuthor(userUtils.getCurrentUser()); + repository.save(post); + var postDTO = postMapper.map(post); + return postDTO; + } + + PostDTO findById(Long id) { + var post = repository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Not Found: " + id)); + var postDTO = postMapper.map(post); + return postDTO; + } + + PostDTO update(PostUpdateDTO postData, Long id) { + var post = repository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Not Found")); + postMapper.update(postData, post); + repository.save(post); + var postDTO = postMapper.map(post); + return postDTO; + } + + void delete(Long id) { + repository.deleteById(id); + } +} diff --git a/src/main/java/io/hexlet/blog/specification/PostCommentSpecification.java b/src/main/java/io/hexlet/blog/specification/PostCommentSpecification.java new file mode 100644 index 0000000..1520aba --- /dev/null +++ b/src/main/java/io/hexlet/blog/specification/PostCommentSpecification.java @@ -0,0 +1,30 @@ +package io.hexlet.blog.specification; + +import java.util.Date; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +import io.hexlet.blog.dto.PostCommentParamsDTO; +import io.hexlet.blog.model.PostComment; + +/** + * PostCommentSpecification + */ +@Component +public class PostCommentSpecification { + public Specification build(PostCommentParamsDTO params) { + Specification spec = Specification.where(null); + return spec + .and(withPostId(params.getPostId())) + .and(withCreatedAtGt(params.getCreatedAtGt())); + } + + private Specification withPostId(Long postId) { + return (root, query, cb) -> postId == null ? cb.conjunction() : cb.equal(root.get("post").get("id"), postId); + } + + private Specification withCreatedAtGt(Date date) { + return (root, query, cb) -> date == null ? cb.conjunction() : cb.greaterThan(root.get("created_at"), date); + } +} diff --git a/src/test/java/io/hexlet/blog/controller/api/PostsCommentsControllerTest.java b/src/test/java/io/hexlet/blog/controller/api/PostsCommentsControllerTest.java new file mode 100644 index 0000000..9dd31b4 --- /dev/null +++ b/src/test/java/io/hexlet/blog/controller/api/PostsCommentsControllerTest.java @@ -0,0 +1,96 @@ +package io.hexlet.blog.controller.api; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor; +import org.springframework.test.web.servlet.MockMvc; + +import io.hexlet.blog.model.Post; +import io.hexlet.blog.repository.PostCommentRepository; +import io.hexlet.blog.repository.PostRepository; +import io.hexlet.blog.util.ModelGenerator; +import io.hexlet.blog.util.UserUtils; +import jakarta.transaction.Transactional; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +public class PostsCommentsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ModelGenerator modelGenerator; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Autowired + private UserUtils userUtils; + + private JwtRequestPostProcessor token; + + private Post testPost; + + @BeforeEach + public void setUp() { + token = jwt().jwt(builder -> builder.subject("hexlet@example.com")); + testPost = Instancio.of(modelGenerator.getPostModel()) + .create(); + testPost.setAuthor(userUtils.getTestUser()); + postRepository.save(testPost); + + var testPost2 = Instancio.of(modelGenerator.getPostModel()) + .create(); + testPost2.setAuthor(userUtils.getTestUser()); + postRepository.save(testPost2); + + var testPostComment = Instancio.of(modelGenerator.getPostCommentModel()).create(); + testPostComment.setPost(testPost); + testPostComment.setAuthor(userUtils.getTestUser()); + postCommentRepository.save(testPostComment); + + var testPostComment2 = Instancio.of(modelGenerator.getPostCommentModel()).create(); + testPostComment2.setPost(testPost2); + testPostComment2.setAuthor(userUtils.getTestUser()); + postCommentRepository.save(testPostComment2); + } + + @Test + public void testIndex() throws Exception { + var result = mockMvc.perform(get("/api/posts_comments").with(token)) + .andExpect(status().isOk()) + .andReturn(); + var body = result.getResponse().getContentAsString(); + assertThatJson(body) + .node("content") + .isArray() + .hasSize(2); + } + + @Test + public void testFilteredIndex() throws Exception { + var result = mockMvc.perform(get("/api/posts_comments?post_id=" + testPost.getId()).with(token)) + .andExpect(status().isOk()) + .andReturn(); + var body = result.getResponse().getContentAsString(); + assertThatJson(body) + .node("content") + .isArray() + .hasSize(1); + } +} + diff --git a/src/test/java/io/hexlet/blog/util/ModelGenerator.java b/src/test/java/io/hexlet/blog/util/ModelGenerator.java index 28c81dd..040836b 100644 --- a/src/test/java/io/hexlet/blog/util/ModelGenerator.java +++ b/src/test/java/io/hexlet/blog/util/ModelGenerator.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import io.hexlet.blog.model.Post; +import io.hexlet.blog.model.PostComment; import io.hexlet.blog.model.User; import jakarta.annotation.PostConstruct; import lombok.Getter; @@ -17,6 +18,7 @@ public class ModelGenerator { private Model postModel; private Model userModel; + private Model postCommentModel; @Autowired private Faker faker; @@ -29,6 +31,11 @@ private void init() { .supply(Select.field(Post::getBody), () -> faker.gameOfThrones().quote()) .toModel(); + postCommentModel = Instancio.of(PostComment.class) + .ignore(Select.field(PostComment::getId)) + .supply(Select.field(Post::getBody), () -> faker.gameOfThrones().quote()) + .toModel(); + userModel = Instancio.of(User.class) .ignore(Select.field(User::getId)) .ignore(Select.field(User::getPosts))