diff --git a/backend/src/main/java/online/wesal/wesal/controller/CommentController.java b/backend/src/main/java/online/wesal/wesal/controller/CommentController.java new file mode 100644 index 0000000..ce80e91 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/controller/CommentController.java @@ -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> 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>> getCommentsByPostId( + @RequestParam Long postId, + Authentication authentication) { + + try { + if (postId == null || postId <= 0) { + return ResponseEntity.badRequest().body(ApiResponse.error("Valid post ID is required")); + } + + List 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")); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/CommentCreateRequestDTO.java b/backend/src/main/java/online/wesal/wesal/dto/CommentCreateRequestDTO.java new file mode 100644 index 0000000..f6874aa --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/dto/CommentCreateRequestDTO.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/CommentResponseDTO.java b/backend/src/main/java/online/wesal/wesal/dto/CommentResponseDTO.java new file mode 100644 index 0000000..3c10c2c --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/dto/CommentResponseDTO.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/entity/Comment.java b/backend/src/main/java/online/wesal/wesal/entity/Comment.java new file mode 100644 index 0000000..6260389 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/entity/Comment.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/repository/CommentRepository.java b/backend/src/main/java/online/wesal/wesal/repository/CommentRepository.java new file mode 100644 index 0000000..a079f7c --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/repository/CommentRepository.java @@ -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 { + + List findByPostIdOrderByCreationDateDesc(Long postId); + + @Query("SELECT c FROM Comment c WHERE c.postId = :postId ORDER BY c.creationDate ASC") + List findByPostIdOrderByCreationDateAsc(@Param("postId") Long postId); + + Optional 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 findRepliesByReplyComment(@Param("replyComment") Long replyComment); +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/service/CommentService.java b/backend/src/main/java/online/wesal/wesal/service/CommentService.java new file mode 100644 index 0000000..f0c84d0 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/service/CommentService.java @@ -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 getCommentsByPostId(Long postId) { + List comments = commentRepository.findByPostIdOrderByCreationDateAsc(postId); + + List creatorIds = comments.stream() + .map(Comment::getCreatorId) + .distinct() + .collect(Collectors.toList()); + + List replyUserIds = comments.stream() + .map(Comment::getReplyUser) + .filter(id -> id != null) + .distinct() + .collect(Collectors.toList()); + + creatorIds.addAll(replyUserIds); + + Map 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()); + } +} \ No newline at end of file