11package com .cognizant .controller ;
22
33import 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 ;
611import java .util .stream .Collectors ;
712
813import org .slf4j .Logger ;
914import org .slf4j .LoggerFactory ;
1015import 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 ;
1322import org .springframework .web .bind .annotation .*;
1423import 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+
1935import io .swagger .v3 .oas .annotations .Operation ;
2036import io .swagger .v3 .oas .annotations .tags .Tag ;
2137
2642public 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