11package com .cognizant .controller ;
22
33import java .io .IOException ;
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 ;
4+ import java .nio .file .*;
5+ import java .util .*;
116import java .util .stream .Collectors ;
127
138import org .slf4j .Logger ;
149import org .slf4j .LoggerFactory ;
1510import org .springframework .beans .factory .annotation .Value ;
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 ;
11+ import org .springframework .core .io .*;
12+ import org .springframework .http .*;
2213import org .springframework .web .bind .annotation .*;
2314import org .springframework .web .multipart .MultipartFile ;
2415
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-
16+ import com .cognizant .dto .*;
17+ import com .cognizant .entities .*;
18+ import com .cognizant .repositries .*;
3519import io .swagger .v3 .oas .annotations .Operation ;
3620import io .swagger .v3 .oas .annotations .tags .Tag ;
3721
4226public class CommentAndAttachmentController {
4327
4428 private final Logger logger = LoggerFactory .getLogger (CommentAndAttachmentController .class );
45-
46- private final CommentRepository commentRepository ;
29+ private final CommentRepository commentRepository ;
4730 private final AttachmentRepository attachmentRepository ;
4831 private final DefectEntityRepository defectRepository ;
4932
50- // Where uploaded files are saved on disk
51- // Set this in application.properties: app.upload.dir=./uploads
52- @ Value ("${app.upload.dir:./uploads}" )
33+ @ Value ("${app.upload.dir:/home/uploads}" )
5334 private String uploadDir ;
5435
55- public CommentAndAttachmentController (
56- CommentRepository commentRepository ,
57- AttachmentRepository attachmentRepository ,
58- DefectEntityRepository defectRepository ) {
59- this .commentRepository = commentRepository ;
36+ public CommentAndAttachmentController (CommentRepository commentRepository ,
37+ AttachmentRepository attachmentRepository ,
38+ DefectEntityRepository defectRepository ) {
39+ this .commentRepository = commentRepository ;
6040 this .attachmentRepository = attachmentRepository ;
61- this .defectRepository = defectRepository ;
41+ this .defectRepository = defectRepository ;
6242 }
6343
64- // ════════════════════════════════════════════════════════════════
65- // COMMENTS
66- // ════════════════════════════════════════════════════════════════
44+ // --- COMMENTS ---
6745
68- // GET /api/defects/{defectId}/comments — list all comments on a bug
69- @ Operation (description = "Get all comments for a defect (oldest first)" )
46+ @ Operation (description = "Get all comments for a defect" )
7047 @ GetMapping ("/defects/{defectId}/comments" )
7148 public ResponseEntity <?> getComments (@ PathVariable Integer defectId ) {
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 );
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" )));
9253 }
9354
94- // POST /api/defects/{defectId}/comments — add a comment
95- @ Operation (description = "Add a comment to a defect (tester or developer)" )
55+ @ Operation (description = "Add a comment to a defect" )
9656 @ PostMapping ("/defects/{defectId}/comments" )
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-
57+ public ResponseEntity <?> addComment (@ PathVariable Integer defectId , @ RequestBody AddCommentRequest request ) {
10358 if (request .getContent () == null || request .getContent ().trim ().isEmpty ()) {
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\" }" );
59+ return ResponseEntity .badRequest ().body (Collections .singletonMap ("error" , "Comment content empty" ));
11260 }
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 );
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 ());
13169 }
13270
133- // DELETE /api/comments/{commentId} — delete a comment (author only)
13471 @ Operation (description = "Delete a comment by ID" )
13572 @ DeleteMapping ("/comments/{commentId}" )
13673 public ResponseEntity <?> deleteComment (@ PathVariable Integer commentId ) {
137- if (!commentRepository .existsById (commentId )) {
138- return ResponseEntity .status (HttpStatus .NOT_FOUND )
139- .body ("{\" error\" : \" Comment not found\" }" );
140- }
74+ if (!commentRepository .existsById (commentId )) return ResponseEntity .notFound ().build ();
14175 commentRepository .deleteById (commentId );
142- return ResponseEntity .ok ("{ \" message\" : \ " Comment deleted\" }" );
76+ return ResponseEntity .ok (Collections . singletonMap ( " message" , "Comment deleted" ) );
14377 }
14478
145- // ════════════════════════════════════════════════════════════════
146- // ATTACHMENTS
147- // ════════════════════════════════════════════════════════════════
79+ // --- ATTACHMENTS ---
14880
149- // GET /api/defects/{defectId}/attachments — list attachments
15081 @ Operation (description = "List all attachments for a defect" )
15182 @ GetMapping ("/defects/{defectId}/attachments" )
15283 public ResponseEntity <?> getAttachments (@ PathVariable Integer defectId ) {
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 );
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 ());
16188 }
16289
163- // POST /api/defects/{defectId}/attachments — upload a file
164- @ Operation (description = "Upload a file attachment to a defect (screenshot, log, etc.)" )
90+ @ Operation (description = "Upload a file attachment" )
16591 @ PostMapping (value = "/defects/{defectId}/attachments" , consumes = MediaType .MULTIPART_FORM_DATA_VALUE )
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
92+ public ResponseEntity <?> uploadAttachment (@ PathVariable Integer defectId , @ RequestParam MultipartFile file , @ RequestParam String uploadedBy ) {
17593 Optional <Defect > opt = defectRepository .findById (defectId );
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- }
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" ));
19296
193- // 4. Save file to disk
19497 try {
19598 Path uploadPath = Paths .get (uploadDir ).toAbsolutePath ().normalize ();
196- Files .createDirectories (uploadPath );
99+ if (! Files . exists ( uploadPath )) Files .createDirectories (uploadPath );
197100
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- : "" ;
101+ String original = file .getOriginalFilename ();
102+ String extension = original != null && original .contains ("." ) ? original .substring (original .lastIndexOf ('.' )) : "" ;
203103 String storedName = UUID .randomUUID ().toString ().replace ("-" , "" ) + extension ;
204104
205- Path targetPath = uploadPath .resolve (storedName );
206- Files .copy (file .getInputStream (), targetPath , StandardCopyOption .REPLACE_EXISTING );
105+ Files .copy (file .getInputStream (), uploadPath .resolve (storedName ), StandardCopyOption .REPLACE_EXISTING );
207106
208- // 5. Save metadata to DB
209107 Attachment attachment = new Attachment ();
210108 attachment .setDefect (opt .get ());
211109 attachment .setOriginalName (original );
@@ -214,63 +112,43 @@ public ResponseEntity<?> uploadAttachment(
214112 attachment .setFileSize (file .getSize ());
215113 attachment .setUploadedBy (uploadedBy );
216114
217- Attachment saved = attachmentRepository .save (attachment );
218- return ResponseEntity .status (HttpStatus .CREATED ).body (toDTO (saved , defectId ));
219-
115+ return ResponseEntity .status (HttpStatus .CREATED ).body (toDTO (attachmentRepository .save (attachment ), defectId ));
220116 } catch (IOException e ) {
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 () + "\" }" );
117+ logger .error ("Storage error" , e );
118+ return ResponseEntity .internalServerError ().body (Collections .singletonMap ("error" , "Failed to save file" ));
224119 }
225120 }
226121
227- // GET /api/attachments/download/{storedName} — download/view file
228- @ Operation (description = "Download or view an attachment by its stored filename" )
122+ @ Operation (description = "Download/View attachment" )
229123 @ GetMapping ("/attachments/download/{storedName}" )
230124 public ResponseEntity <Resource > downloadAttachment (@ PathVariable String storedName ) {
231125 try {
232126 Path filePath = Paths .get (uploadDir ).toAbsolutePath ().normalize ().resolve (storedName );
233127 Resource resource = new UrlResource (filePath .toUri ());
234-
235- if (!resource .exists ()) {
236- return ResponseEntity .notFound ().build ();
237- }
128+ if (!resource .exists ()) return ResponseEntity .notFound ().build ();
238129
239130 String contentType = Files .probeContentType (filePath );
240- if (contentType == null ) contentType = "application/octet-stream" ;
241-
242131 return ResponseEntity .ok ()
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- }
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 (); }
250136 }
251137
252- // DELETE /api/attachments/{attachmentId} — delete an attachment
253- @ Operation (description = "Delete an attachment by ID (also removes file from disk)" )
138+ @ Operation (description = "Delete attachment" )
254139 @ DeleteMapping ("/attachments/{attachmentId}" )
255140 public ResponseEntity <?> deleteAttachment (@ PathVariable Integer attachmentId ) {
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\" }" );
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 ());
271148 }
272149
273- // Helper — entity to DTO
150+ // --- HELPERS ---
151+
274152 private AttachmentDTO toDTO (Attachment a , Integer defectId ) {
275153 AttachmentDTO dto = new AttachmentDTO ();
276154 dto .setId (a .getId ());
@@ -284,4 +162,15 @@ private AttachmentDTO toDTO(Attachment a, Integer defectId) {
284162 dto .setDownloadUrl ("/api/attachments/download/" + a .getStoredName ());
285163 return dto ;
286164 }
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+ }
287176}
0 commit comments