Skip to content

Commit ed6f523

Browse files
authored
Update CommentAndAttachmentController.java
1 parent b245185 commit ed6f523

1 file changed

Lines changed: 187 additions & 76 deletions

File tree

Lines changed: 187 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
package com.cognizant.controller;
22

33
import java.io.IOException;
4-
import java.nio.file.*;
5-
import java.util.*;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.nio.file.Paths;
7+
import java.nio.file.StandardCopyOption;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import java.util.UUID;
611
import java.util.stream.Collectors;
712

813
import org.slf4j.Logger;
914
import org.slf4j.LoggerFactory;
1015
import org.springframework.beans.factory.annotation.Value;
11-
import org.springframework.core.io.*;
12-
import org.springframework.http.*;
16+
import org.springframework.core.io.Resource;
17+
import org.springframework.core.io.UrlResource;
18+
import org.springframework.http.HttpHeaders;
19+
import org.springframework.http.HttpStatus;
20+
import org.springframework.http.MediaType;
21+
import org.springframework.http.ResponseEntity;
1322
import org.springframework.web.bind.annotation.*;
1423
import org.springframework.web.multipart.MultipartFile;
1524

16-
import com.cognizant.dto.*;
17-
import com.cognizant.entities.*;
18-
import com.cognizant.repositries.*;
25+
import com.cognizant.dto.AddCommentRequest;
26+
import com.cognizant.dto.AttachmentDTO;
27+
import com.cognizant.dto.CommentDTO;
28+
import com.cognizant.entities.Attachment;
29+
import com.cognizant.entities.Comment;
30+
import com.cognizant.entities.Defect;
31+
import com.cognizant.repositries.AttachmentRepository;
32+
import com.cognizant.repositries.CommentRepository;
33+
import com.cognizant.repositries.DefectEntityRepository;
34+
1935
import io.swagger.v3.oas.annotations.Operation;
2036
import io.swagger.v3.oas.annotations.tags.Tag;
2137

@@ -26,84 +42,170 @@
2642
public class CommentAndAttachmentController {
2743

2844
private final Logger logger = LoggerFactory.getLogger(CommentAndAttachmentController.class);
29-
private final CommentRepository commentRepository;
45+
46+
private final CommentRepository commentRepository;
3047
private final AttachmentRepository attachmentRepository;
3148
private final DefectEntityRepository defectRepository;
3249

33-
@Value("${app.upload.dir:/home/uploads}")
50+
// Where uploaded files are saved on disk
51+
// Set this in application.properties: app.upload.dir=./uploads
52+
@Value("${app.upload.dir:./uploads}")
3453
private String uploadDir;
3554

36-
public CommentAndAttachmentController(CommentRepository commentRepository,
37-
AttachmentRepository attachmentRepository,
38-
DefectEntityRepository defectRepository) {
39-
this.commentRepository = commentRepository;
55+
public CommentAndAttachmentController(
56+
CommentRepository commentRepository,
57+
AttachmentRepository attachmentRepository,
58+
DefectEntityRepository defectRepository) {
59+
this.commentRepository = commentRepository;
4060
this.attachmentRepository = attachmentRepository;
41-
this.defectRepository = defectRepository;
61+
this.defectRepository = defectRepository;
4262
}
4363

44-
// --- COMMENTS ---
64+
// ════════════════════════════════════════════════════════════════
65+
// COMMENTS
66+
// ════════════════════════════════════════════════════════════════
4567

46-
@Operation(description = "Get all comments for a defect")
68+
// GET /api/defects/{defectId}/comments — list all comments on a bug
69+
@Operation(description = "Get all comments for a defect (oldest first)")
4770
@GetMapping("/defects/{defectId}/comments")
4871
public ResponseEntity<?> getComments(@PathVariable Integer defectId) {
49-
return defectRepository.findById(defectId)
50-
.map(defect -> ResponseEntity.ok(commentRepository.findByDefectOrderByCreatedAtAsc(defect)
51-
.stream().map(c -> toCommentDTO(c, defectId)).collect(Collectors.toList())))
52-
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.singletonMap("error", "Defect not found")));
72+
Optional<Defect> opt = defectRepository.findById(defectId);
73+
if (opt.isEmpty()) {
74+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
75+
.body("{\"error\": \"Defect not found\"}");
76+
}
77+
List<Comment> comments = commentRepository
78+
.findByDefectOrderByCreatedAtAsc(opt.get());
79+
80+
List<CommentDTO> dtos = comments.stream().map(c -> {
81+
CommentDTO dto = new CommentDTO();
82+
dto.setId(c.getId());
83+
dto.setDefectId(defectId);
84+
dto.setAuthor(c.getAuthor());
85+
dto.setAuthorRole(c.getAuthorRole());
86+
dto.setContent(c.getContent());
87+
dto.setCreatedAt(c.getCreatedAt());
88+
return dto;
89+
}).collect(Collectors.toList());
90+
91+
return ResponseEntity.ok(dtos);
5392
}
5493

55-
@Operation(description = "Add a comment to a defect")
94+
// POST /api/defects/{defectId}/comments — add a comment
95+
@Operation(description = "Add a comment to a defect (tester or developer)")
5696
@PostMapping("/defects/{defectId}/comments")
57-
public ResponseEntity<?> addComment(@PathVariable Integer defectId, @RequestBody AddCommentRequest request) {
97+
public ResponseEntity<?> addComment(
98+
@PathVariable Integer defectId,
99+
@RequestBody AddCommentRequest request) {
100+
101+
logger.info("Adding comment to defect #{} by {}", defectId, request.getAuthor());
102+
58103
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
59-
return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Comment content empty"));
104+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
105+
.body("{\"error\": \"Comment content cannot be empty\"}");
106+
}
107+
108+
Optional<Defect> opt = defectRepository.findById(defectId);
109+
if (opt.isEmpty()) {
110+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
111+
.body("{\"error\": \"Defect not found\"}");
60112
}
61-
return defectRepository.findById(defectId).map(defect -> {
62-
Comment comment = new Comment();
63-
comment.setDefect(defect);
64-
comment.setAuthor(request.getAuthor());
65-
comment.setAuthorRole(request.getAuthorRole());
66-
comment.setContent(request.getContent().trim());
67-
return ResponseEntity.status(HttpStatus.CREATED).body(toCommentDTO(commentRepository.save(comment), defectId));
68-
}).orElse(ResponseEntity.notFound().build());
113+
114+
Comment comment = new Comment();
115+
comment.setDefect(opt.get());
116+
comment.setAuthor(request.getAuthor());
117+
comment.setAuthorRole(request.getAuthorRole());
118+
comment.setContent(request.getContent().trim());
119+
120+
Comment saved = commentRepository.save(comment);
121+
122+
CommentDTO dto = new CommentDTO();
123+
dto.setId(saved.getId());
124+
dto.setDefectId(defectId);
125+
dto.setAuthor(saved.getAuthor());
126+
dto.setAuthorRole(saved.getAuthorRole());
127+
dto.setContent(saved.getContent());
128+
dto.setCreatedAt(saved.getCreatedAt());
129+
130+
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
69131
}
70132

133+
// DELETE /api/comments/{commentId} — delete a comment (author only)
71134
@Operation(description = "Delete a comment by ID")
72135
@DeleteMapping("/comments/{commentId}")
73136
public ResponseEntity<?> deleteComment(@PathVariable Integer commentId) {
74-
if (!commentRepository.existsById(commentId)) return ResponseEntity.notFound().build();
137+
if (!commentRepository.existsById(commentId)) {
138+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
139+
.body("{\"error\": \"Comment not found\"}");
140+
}
75141
commentRepository.deleteById(commentId);
76-
return ResponseEntity.ok(Collections.singletonMap("message", "Comment deleted"));
142+
return ResponseEntity.ok("{\"message\": \"Comment deleted\"}");
77143
}
78144

79-
// --- ATTACHMENTS ---
145+
// ════════════════════════════════════════════════════════════════
146+
// ATTACHMENTS
147+
// ════════════════════════════════════════════════════════════════
80148

149+
// GET /api/defects/{defectId}/attachments — list attachments
81150
@Operation(description = "List all attachments for a defect")
82151
@GetMapping("/defects/{defectId}/attachments")
83152
public ResponseEntity<?> getAttachments(@PathVariable Integer defectId) {
84-
return defectRepository.findById(defectId)
85-
.map(defect -> ResponseEntity.ok(attachmentRepository.findByDefect(defect)
86-
.stream().map(a -> toDTO(a, defectId)).collect(Collectors.toList())))
87-
.orElse(ResponseEntity.notFound().build());
153+
Optional<Defect> opt = defectRepository.findById(defectId);
154+
if (opt.isEmpty()) {
155+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
156+
.body("{\"error\": \"Defect not found\"}");
157+
}
158+
List<Attachment> attachments = attachmentRepository.findByDefect(opt.get());
159+
List<AttachmentDTO> dtos = attachments.stream().map(a -> toDTO(a, defectId)).collect(Collectors.toList());
160+
return ResponseEntity.ok(dtos);
88161
}
89162

90-
@Operation(description = "Upload a file attachment")
163+
// POST /api/defects/{defectId}/attachments — upload a file
164+
@Operation(description = "Upload a file attachment to a defect (screenshot, log, etc.)")
91165
@PostMapping(value = "/defects/{defectId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
92-
public ResponseEntity<?> uploadAttachment(@PathVariable Integer defectId, @RequestParam MultipartFile file, @RequestParam String uploadedBy) {
166+
public ResponseEntity<?> uploadAttachment(
167+
@PathVariable Integer defectId,
168+
@RequestParam MultipartFile file,
169+
@RequestParam String uploadedBy) {
170+
171+
logger.info("Uploading attachment '{}' for defect #{} by {}",
172+
file.getOriginalFilename(), defectId, uploadedBy);
173+
174+
// 1. Validate defect exists
93175
Optional<Defect> opt = defectRepository.findById(defectId);
94-
if (opt.isEmpty()) return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.singletonMap("error", "Defect not found"));
95-
if (file.isEmpty()) return ResponseEntity.badRequest().body(Collections.singletonMap("error", "File is empty"));
176+
if (opt.isEmpty()) {
177+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
178+
.body("{\"error\": \"Defect not found\"}");
179+
}
180+
181+
// 2. Validate file not empty
182+
if (file.isEmpty()) {
183+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
184+
.body("{\"error\": \"File is empty\"}");
185+
}
186+
187+
// 3. Validate file size (max 10 MB)
188+
if (file.getSize() > 10 * 1024 * 1024) {
189+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
190+
.body("{\"error\": \"File too large. Max 10MB.\"}");
191+
}
96192

193+
// 4. Save file to disk
97194
try {
98195
Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
99-
if (!Files.exists(uploadPath)) Files.createDirectories(uploadPath);
196+
Files.createDirectories(uploadPath);
100197

101-
String original = file.getOriginalFilename();
102-
String extension = original != null && original.contains(".") ? original.substring(original.lastIndexOf('.')) : "";
198+
// Generate a unique filename to avoid collisions
199+
String original = file.getOriginalFilename();
200+
String extension = original != null && original.contains(".")
201+
? original.substring(original.lastIndexOf('.'))
202+
: "";
103203
String storedName = UUID.randomUUID().toString().replace("-", "") + extension;
104204

105-
Files.copy(file.getInputStream(), uploadPath.resolve(storedName), StandardCopyOption.REPLACE_EXISTING);
205+
Path targetPath = uploadPath.resolve(storedName);
206+
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
106207

208+
// 5. Save metadata to DB
107209
Attachment attachment = new Attachment();
108210
attachment.setDefect(opt.get());
109211
attachment.setOriginalName(original);
@@ -112,43 +214,63 @@ public ResponseEntity<?> uploadAttachment(@PathVariable Integer defectId, @Reque
112214
attachment.setFileSize(file.getSize());
113215
attachment.setUploadedBy(uploadedBy);
114216

115-
return ResponseEntity.status(HttpStatus.CREATED).body(toDTO(attachmentRepository.save(attachment), defectId));
217+
Attachment saved = attachmentRepository.save(attachment);
218+
return ResponseEntity.status(HttpStatus.CREATED).body(toDTO(saved, defectId));
219+
116220
} catch (IOException e) {
117-
logger.error("Storage error", e);
118-
return ResponseEntity.internalServerError().body(Collections.singletonMap("error", "Failed to save file"));
221+
logger.error("Failed to save file", e);
222+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
223+
.body("{\"error\": \"Failed to save file: " + e.getMessage() + "\"}");
119224
}
120225
}
121226

122-
@Operation(description = "Download/View attachment")
227+
// GET /api/attachments/download/{storedName} — download/view file
228+
@Operation(description = "Download or view an attachment by its stored filename")
123229
@GetMapping("/attachments/download/{storedName}")
124230
public ResponseEntity<Resource> downloadAttachment(@PathVariable String storedName) {
125231
try {
126232
Path filePath = Paths.get(uploadDir).toAbsolutePath().normalize().resolve(storedName);
127233
Resource resource = new UrlResource(filePath.toUri());
128-
if (!resource.exists()) return ResponseEntity.notFound().build();
234+
235+
if (!resource.exists()) {
236+
return ResponseEntity.notFound().build();
237+
}
129238

130239
String contentType = Files.probeContentType(filePath);
240+
if (contentType == null) contentType = "application/octet-stream";
241+
131242
return ResponseEntity.ok()
132-
.contentType(MediaType.parseMediaType(contentType != null ? contentType : "application/octet-stream"))
133-
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + storedName + "\"")
134-
.body(resource);
135-
} catch (IOException e) { return ResponseEntity.internalServerError().build(); }
243+
.contentType(MediaType.parseMediaType(contentType))
244+
.header(HttpHeaders.CONTENT_DISPOSITION,
245+
"inline; filename=\"" + resource.getFilename() + "\"")
246+
.body(resource);
247+
} catch (IOException e) {
248+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
249+
}
136250
}
137251

138-
@Operation(description = "Delete attachment")
252+
// DELETE /api/attachments/{attachmentId} — delete an attachment
253+
@Operation(description = "Delete an attachment by ID (also removes file from disk)")
139254
@DeleteMapping("/attachments/{attachmentId}")
140255
public ResponseEntity<?> deleteAttachment(@PathVariable Integer attachmentId) {
141-
return attachmentRepository.findById(attachmentId).map(a -> {
142-
try {
143-
Files.deleteIfExists(Paths.get(uploadDir).resolve(a.getStoredName()));
144-
} catch (IOException e) { logger.warn("Disk deletion failed: {}", e.getMessage()); }
145-
attachmentRepository.deleteById(attachmentId);
146-
return ResponseEntity.ok(Collections.singletonMap("message", "Deleted"));
147-
}).orElse(ResponseEntity.notFound().build());
256+
Optional<Attachment> opt = attachmentRepository.findById(attachmentId);
257+
if (opt.isEmpty()) {
258+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
259+
.body("{\"error\": \"Attachment not found\"}");
260+
}
261+
// Delete file from disk
262+
try {
263+
Path filePath = Paths.get(uploadDir).toAbsolutePath().normalize()
264+
.resolve(opt.get().getStoredName());
265+
Files.deleteIfExists(filePath);
266+
} catch (IOException e) {
267+
logger.warn("Could not delete file from disk: {}", e.getMessage());
268+
}
269+
attachmentRepository.deleteById(attachmentId);
270+
return ResponseEntity.ok("{\"message\": \"Attachment deleted\"}");
148271
}
149272

150-
// --- HELPERS ---
151-
273+
// Helper — entity to DTO
152274
private AttachmentDTO toDTO(Attachment a, Integer defectId) {
153275
AttachmentDTO dto = new AttachmentDTO();
154276
dto.setId(a.getId());
@@ -162,15 +284,4 @@ private AttachmentDTO toDTO(Attachment a, Integer defectId) {
162284
dto.setDownloadUrl("/api/attachments/download/" + a.getStoredName());
163285
return dto;
164286
}
165-
166-
private CommentDTO toCommentDTO(Comment c, Integer defectId) {
167-
CommentDTO dto = new CommentDTO();
168-
dto.setId(c.getId());
169-
dto.setDefectId(defectId);
170-
dto.setAuthor(c.getAuthor());
171-
dto.setAuthorRole(c.getAuthorRole());
172-
dto.setContent(c.getContent());
173-
dto.setCreatedAt(c.getCreatedAt());
174-
return dto;
175-
}
176287
}

0 commit comments

Comments
 (0)