1+ package com .example .demo .controller ;
2+
3+ import org .springframework .web .bind .annotation .RestController ;
4+ import org .springframework .web .bind .annotation .GetMapping ;
5+ import org .springframework .web .bind .annotation .RequestParam ;
6+ import org .springframework .web .bind .annotation .RequestMapping ;
7+ import org .springframework .beans .factory .annotation .Autowired ;
8+ import org .springframework .http .ResponseEntity ;
9+ import org .springframework .http .HttpStatus ;
10+ import com .example .demo .service .FilmService ;
11+ import com .example .demo .dto .FilmResponse ;
12+
13+ import io .swagger .v3 .oas .annotations .Operation ;
14+ import io .swagger .v3 .oas .annotations .Parameter ;
15+ import io .swagger .v3 .oas .annotations .responses .ApiResponse ;
16+ import io .swagger .v3 .oas .annotations .responses .ApiResponses ;
17+ import io .swagger .v3 .oas .annotations .tags .Tag ;
18+ import io .swagger .v3 .oas .annotations .media .Content ;
19+ import io .swagger .v3 .oas .annotations .media .Schema ;
20+
21+ import java .util .Map ;
22+ import java .util .HashMap ;
23+ import java .util .List ;
24+
25+ /**
26+ * FilmController - REST API Controller for Film Query Operations
27+ *
28+ * This controller provides REST endpoints for querying films from the Sakila database.
29+ * It implements the Film Query API specification for retrieving films that start with
30+ * specific letters.
31+ *
32+ * API Endpoints:
33+ * - GET /api/v1/films - Retrieve all films or filter by starting letter
34+ * - GET /api/v1/films?startsWith=A - Retrieve films starting with letter "A"
35+ *
36+ * Task 4.1: Create FilmController class with @RestController annotation ✅
37+ * Task 4.2: Implement GET /api/v1/films endpoint with @GetMapping ✅
38+ * Task 4.3: Add startsWith parameter with @RequestParam validation ✅
39+ * Task 4.4: Implement parameter validation logic (single letter, not empty) ✅
40+ * Task 4.5: Create FilmResponse DTO for JSON response format ✅
41+ * Task 4.6: Implement response formatting with films array, count, and filter ✅
42+ * Task 4.7: Add OpenAPI @Operation, @Parameter, and @ApiResponse annotations ✅
43+ * Task 4.8: Implement proper HTTP status code handling ✅
44+ */
45+ @ RestController
46+ @ RequestMapping ("/api/v1" )
47+ @ Tag (name = "Films" , description = "Film query operations" )
48+ public class FilmController {
49+
50+ private final FilmService filmService ;
51+
52+ @ Autowired
53+ public FilmController (FilmService filmService ) {
54+ this .filmService = filmService ;
55+ }
56+
57+ /**
58+ * Task 4.2: Implement GET /api/v1/films endpoint with @GetMapping ✅
59+ * Task 4.3: Add startsWith parameter with @RequestParam validation ✅
60+ * Task 4.4: Implement parameter validation logic (single letter, not empty) ✅
61+ * Task 4.6: Implement response formatting with films array, count, and filter ✅
62+ * Task 4.7: Add OpenAPI @Operation, @Parameter, and @ApiResponse annotations ✅
63+ * Task 4.8: Implement proper HTTP status code handling ✅
64+ *
65+ * Retrieves films from the Sakila database, optionally filtered by starting letter.
66+ *
67+ * @param startsWith Optional parameter to filter films by starting letter (single character A-Z)
68+ * @return JSON response containing films array, count, and filter information
69+ * @throws IllegalArgumentException if startsWith parameter is invalid (not a single letter)
70+ */
71+ @ Operation (
72+ summary = "Query films by starting letter" ,
73+ description = """
74+ Retrieves films from the Sakila database that start with the specified letter.
75+ The query is case-insensitive and returns film ID and title for each matching film.
76+
77+ **Performance**: Query execution time is guaranteed to be under 2 seconds.
78+
79+ **Expected Results**:
80+ - Letter "A": 46 films
81+ - Letter "B": 54 films
82+ - Letter "C": 58 films
83+ """ ,
84+ operationId = "getFilmsByStartingLetter"
85+ )
86+ @ ApiResponses (value = {
87+ @ ApiResponse (
88+ responseCode = "200" ,
89+ description = "Successfully retrieved films" ,
90+ content = @ Content (
91+ mediaType = "application/json" ,
92+ schema = @ Schema (implementation = FilmResponse .class )
93+ )
94+ ),
95+ @ ApiResponse (
96+ responseCode = "400" ,
97+ description = "Invalid parameter - startsWith must be a single letter (A-Z)" ,
98+ content = @ Content (
99+ mediaType = "application/json" ,
100+ schema = @ Schema (implementation = org .springframework .http .ProblemDetail .class )
101+ )
102+ ),
103+ @ ApiResponse (
104+ responseCode = "500" ,
105+ description = "Internal server error" ,
106+ content = @ Content (
107+ mediaType = "application/json" ,
108+ schema = @ Schema (implementation = org .springframework .http .ProblemDetail .class )
109+ )
110+ )
111+ })
112+ @ GetMapping ("/films" )
113+ public ResponseEntity <FilmResponse > getFilms (
114+ @ Parameter (
115+ name = "startsWith" ,
116+ description = "Filter films by starting letter (case-insensitive, single character A-Z)" ,
117+ example = "A" ,
118+ schema = @ Schema (type = "string" , pattern = "^[A-Za-z]$" )
119+ )
120+ @ RequestParam (required = false ) String startsWith ) {
121+
122+ // Task 4.4: Implement parameter validation logic (single letter, not empty)
123+ if (startsWith != null ) {
124+ validateStartsWithParameter (startsWith );
125+ }
126+
127+ // Call service layer to get films
128+ List <Map <String , Object >> films = filmService .findFilmsByStartingLetter (startsWith );
129+
130+ // Task 4.6: Implement response formatting with films array, count, and filter
131+ // Build filter object
132+ Map <String , Object > filter = new HashMap <>();
133+ if (startsWith != null && !startsWith .trim ().isEmpty ()) {
134+ filter .put ("startsWith" , startsWith );
135+ }
136+
137+ // Create FilmResponse DTO (Task 4.5)
138+ FilmResponse response = new FilmResponse (films , films .size (), filter );
139+
140+ // Task 4.8: Implement proper HTTP status code handling
141+ return ResponseEntity .ok (response );
142+ }
143+
144+ /**
145+ * Task 4.4: Implement parameter validation logic (single letter, not empty)
146+ *
147+ * Validates the startsWith parameter to ensure it meets the API requirements:
148+ * - Must be a single character
149+ * - Must be a letter (A-Z, a-z)
150+ * - Cannot be empty or whitespace
151+ * - Cannot be numeric or special characters
152+ *
153+ * @param startsWith The parameter value to validate
154+ * @throws IllegalArgumentException if the parameter is invalid
155+ */
156+ private void validateStartsWithParameter (String startsWith ) {
157+ if (startsWith == null || startsWith .trim ().isEmpty ()) {
158+ throw new IllegalArgumentException ("Parameter 'startsWith' cannot be empty" );
159+ }
160+
161+ String trimmed = startsWith .trim ();
162+
163+ // Check if it's a single character
164+ if (trimmed .length () != 1 ) {
165+ throw new IllegalArgumentException ("Parameter 'startsWith' must be a single letter (A-Z)" );
166+ }
167+
168+ char character = trimmed .charAt (0 );
169+
170+ // Check if it's a letter (A-Z, a-z)
171+ if (!Character .isLetter (character )) {
172+ throw new IllegalArgumentException ("Parameter 'startsWith' must be a single letter (A-Z)" );
173+ }
174+ }
175+ }
0 commit comments