feat: creating comments endpoint (backend)

This commit is contained in:
sBubshait 2025-07-27 10:37:56 +03:00
parent a6babf18ac
commit 304ba5c731
6 changed files with 516 additions and 0 deletions

View File

@ -0,0 +1,88 @@
package online.wesal.wesal.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import online.wesal.wesal.dto.ApiResponse;
import online.wesal.wesal.dto.CommentCreateRequestDTO;
import online.wesal.wesal.dto.CommentResponseDTO;
import java.util.List;
import online.wesal.wesal.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/posts/comments")
@CrossOrigin(origins = "*")
@Tag(name = "Comments", description = "Comment management endpoints")
public class CommentController {
@Autowired
private CommentService commentService;
@PostMapping(value = "/create", consumes = "application/json", produces = "application/json")
@Operation(summary = "Create comment", description = "Create a new comment or reply to an existing comment with 3-level nesting")
public ResponseEntity<ApiResponse<CommentResponseDTO>> createComment(
@Valid @RequestBody CommentCreateRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("postId".equals(field)) return "Valid post ID is required";
if ("body".equals(field)) return "Comment body is required and cannot exceed 1000 characters";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
CommentResponseDTO response = commentService.createComment(
request.getPostId(),
request.getBody(),
request.getReplyComment()
);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("Post not found")) {
message = "Post not found";
} else if (e.getMessage().contains("Parent comment not found")) {
message = "Comment you're replying to not found";
} else if (e.getMessage().contains("User not found") || e.getMessage().contains("creator not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@GetMapping("/list")
@Operation(summary = "Get comments for post", description = "Get all comments for a specific post ordered by creation date")
public ResponseEntity<ApiResponse<List<CommentResponseDTO>>> getCommentsByPostId(
@RequestParam Long postId,
Authentication authentication) {
try {
if (postId == null || postId <= 0) {
return ResponseEntity.badRequest().body(ApiResponse.error("Valid post ID is required"));
}
List<CommentResponseDTO> response = commentService.getCommentsByPostId(postId);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
}

View File

@ -0,0 +1,54 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class CommentCreateRequestDTO {
@NotNull(message = "Post ID is required")
private Long postId;
@NotBlank(message = "Comment body is required")
@Size(max = 1000, message = "Comment body cannot exceed 1000 characters")
private String body;
private Long replyComment;
public CommentCreateRequestDTO() {}
public CommentCreateRequestDTO(Long postId, String body) {
this.postId = postId;
this.body = body;
}
public CommentCreateRequestDTO(Long postId, String body, Long replyComment) {
this.postId = postId;
this.body = body;
this.replyComment = replyComment;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Long getReplyComment() {
return replyComment;
}
public void setReplyComment(Long replyComment) {
this.replyComment = replyComment;
}
}

View File

@ -0,0 +1,104 @@
package online.wesal.wesal.dto;
import online.wesal.wesal.entity.Comment;
import online.wesal.wesal.entity.User;
import java.time.LocalDateTime;
public class CommentResponseDTO {
private String id;
private String postId;
private CreatorDTO creator;
private String body;
private LocalDateTime creationDate;
private String replyComment;
private String displayReplyComment;
private CreatorDTO replyUser;
private Integer level;
public CommentResponseDTO() {}
public CommentResponseDTO(Comment comment, User creator, User replyUser) {
this.id = String.valueOf(comment.getId());
this.postId = String.valueOf(comment.getPostId());
this.creator = new CreatorDTO(creator);
this.body = comment.getBody();
this.creationDate = comment.getCreationDate();
this.replyComment = comment.getReplyComment() != null ? String.valueOf(comment.getReplyComment()) : null;
this.displayReplyComment = comment.getDisplayReplyComment() != null ? String.valueOf(comment.getDisplayReplyComment()) : null;
this.replyUser = replyUser != null ? new CreatorDTO(replyUser) : null;
this.level = comment.getLevel();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPostId() {
return postId;
}
public void setPostId(String postId) {
this.postId = postId;
}
public CreatorDTO getCreator() {
return creator;
}
public void setCreator(CreatorDTO creator) {
this.creator = creator;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public LocalDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = creationDate;
}
public String getReplyComment() {
return replyComment;
}
public void setReplyComment(String replyComment) {
this.replyComment = replyComment;
}
public String getDisplayReplyComment() {
return displayReplyComment;
}
public void setDisplayReplyComment(String displayReplyComment) {
this.displayReplyComment = displayReplyComment;
}
public CreatorDTO getReplyUser() {
return replyUser;
}
public void setReplyUser(CreatorDTO replyUser) {
this.replyUser = replyUser;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
}

View File

@ -0,0 +1,130 @@
package online.wesal.wesal.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name = "post_id")
private Long postId;
@Column(nullable = false, name = "creator_id")
private Long creatorId;
@Column(nullable = false, length = 1000)
@NotBlank
private String body;
@Column(nullable = false)
private LocalDateTime creationDate = LocalDateTime.now();
@Column(name = "reply_comment")
private Long replyComment;
@Column(name = "display_reply_comment")
private Long displayReplyComment;
@Column(name = "reply_user")
private Long replyUser;
@Column(nullable = false)
private Integer level = 1;
public Comment() {}
public Comment(Long postId, Long creatorId, String body) {
this.postId = postId;
this.creatorId = creatorId;
this.body = body;
this.level = 1;
}
public Comment(Long postId, Long creatorId, String body, Long replyComment, Long displayReplyComment, Long replyUser, Integer level) {
this.postId = postId;
this.creatorId = creatorId;
this.body = body;
this.replyComment = replyComment;
this.displayReplyComment = displayReplyComment;
this.replyUser = replyUser;
this.level = level;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
public Long getCreatorId() {
return creatorId;
}
public void setCreatorId(Long creatorId) {
this.creatorId = creatorId;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public LocalDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = creationDate;
}
public Long getReplyComment() {
return replyComment;
}
public void setReplyComment(Long replyComment) {
this.replyComment = replyComment;
}
public Long getDisplayReplyComment() {
return displayReplyComment;
}
public void setDisplayReplyComment(Long displayReplyComment) {
this.displayReplyComment = displayReplyComment;
}
public Long getReplyUser() {
return replyUser;
}
public void setReplyUser(Long replyUser) {
this.replyUser = replyUser;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
}

View File

@ -0,0 +1,26 @@
package online.wesal.wesal.repository;
import online.wesal.wesal.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPostIdOrderByCreationDateDesc(Long postId);
@Query("SELECT c FROM Comment c WHERE c.postId = :postId ORDER BY c.creationDate ASC")
List<Comment> findByPostIdOrderByCreationDateAsc(@Param("postId") Long postId);
Optional<Comment> findByIdAndPostId(Long id, Long postId);
long countByPostId(Long postId);
@Query("SELECT c FROM Comment c WHERE c.replyComment = :replyComment ORDER BY c.creationDate ASC")
List<Comment> findRepliesByReplyComment(@Param("replyComment") Long replyComment);
}

View File

@ -0,0 +1,114 @@
package online.wesal.wesal.service;
import online.wesal.wesal.dto.CommentResponseDTO;
import online.wesal.wesal.entity.Comment;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.entity.User;
import online.wesal.wesal.repository.CommentRepository;
import online.wesal.wesal.repository.PostRepository;
import online.wesal.wesal.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
@Transactional
public CommentResponseDTO createComment(Long postId, String body, Long replyComment) {
User currentUser = userService.getCurrentUser();
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
Comment comment;
User replyUser = null;
if (replyComment == null) {
comment = new Comment(postId, currentUser.getId(), body);
} else {
Comment parentComment = commentRepository.findByIdAndPostId(replyComment, postId)
.orElseThrow(() -> new RuntimeException("Parent comment not found"));
replyUser = userRepository.findById(parentComment.getCreatorId())
.orElseThrow(() -> new RuntimeException("Parent comment creator not found"));
Integer newLevel;
Long displayReplyComment;
if (parentComment.getLevel() >= 3) {
newLevel = 3;
Comment rootComment = findRootCommentForLevel3(parentComment);
displayReplyComment = rootComment.getId();
} else {
newLevel = parentComment.getLevel() + 1;
displayReplyComment = replyComment;
}
comment = new Comment(postId, currentUser.getId(), body, replyComment, displayReplyComment, replyUser.getId(), newLevel);
}
comment = commentRepository.save(comment);
post.setComments(post.getComments() + 1);
postRepository.save(post);
return new CommentResponseDTO(comment, currentUser, replyUser);
}
private Comment findRootCommentForLevel3(Comment comment) {
if (comment.getLevel() <= 2) {
return comment;
}
if (comment.getReplyComment() != null) {
Comment parentComment = commentRepository.findById(comment.getReplyComment())
.orElse(comment);
return findRootCommentForLevel3(parentComment);
}
return comment;
}
public List<CommentResponseDTO> getCommentsByPostId(Long postId) {
List<Comment> comments = commentRepository.findByPostIdOrderByCreationDateAsc(postId);
List<Long> creatorIds = comments.stream()
.map(Comment::getCreatorId)
.distinct()
.collect(Collectors.toList());
List<Long> replyUserIds = comments.stream()
.map(Comment::getReplyUser)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
creatorIds.addAll(replyUserIds);
Map<Long, User> users = userRepository.findAllById(creatorIds.stream().distinct().collect(Collectors.toList())).stream()
.collect(Collectors.toMap(User::getId, user -> user));
return comments.stream()
.map(comment -> new CommentResponseDTO(comment,
users.get(comment.getCreatorId()),
comment.getReplyUser() != null ? users.get(comment.getReplyUser()) : null))
.collect(Collectors.toList());
}
}