Skip to content

Commit a3bfbab

Browse files
committed
Completed Task 4
1 parent 0e4ebc4 commit a3bfbab

File tree

10 files changed

+712
-28
lines changed

10 files changed

+712
-28
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.example.demo.controller;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ProblemDetail;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.ControllerAdvice;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import java.net.URI;
13+
import java.time.Instant;
14+
import java.util.UUID;
15+
16+
/**
17+
* GlobalExceptionHandler - Centralized exception handling for Film Query API
18+
*
19+
* Task 4.8: Implement proper HTTP status code handling
20+
* Task 10.1: Create GlobalExceptionHandler class with @ControllerAdvice
21+
* Task 10.2: Implement handleIllegalArgumentException method
22+
* Task 10.3: Implement RFC 7807 ProblemDetail response format
23+
* Task 10.4: Add proper HTTP status code mapping (400, 500)
24+
* Task 10.5: Implement descriptive error messages for invalid parameters
25+
*
26+
* This class provides centralized exception handling following RFC 7807 Problem Details
27+
* for HTTP APIs. It ensures consistent error responses across all endpoints.
28+
*
29+
* Error Response Format (RFC 7807):
30+
* {
31+
* "type": "https://example.com/problems/invalid-parameter",
32+
* "title": "Invalid Parameter",
33+
* "status": 400,
34+
* "detail": "Parameter 'startsWith' must be a single letter (A-Z)",
35+
* "instance": "/api/v1/films",
36+
* "timestamp": "2024-01-15T10:30:00Z"
37+
* }
38+
*/
39+
@ControllerAdvice
40+
public class GlobalExceptionHandler {
41+
42+
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
43+
44+
/**
45+
* Task 10.2: Implement handleIllegalArgumentException method
46+
*
47+
* Handles IllegalArgumentException for parameter validation errors.
48+
* Returns HTTP 400 Bad Request with RFC 7807 Problem Details format.
49+
*/
50+
@ExceptionHandler(IllegalArgumentException.class)
51+
public ResponseEntity<ProblemDetail> handleIllegalArgumentException(
52+
IllegalArgumentException ex, HttpServletRequest request) {
53+
54+
logger.warn("Invalid parameter request: {}", ex.getMessage());
55+
56+
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
57+
HttpStatus.BAD_REQUEST, ex.getMessage()
58+
);
59+
60+
problemDetail.setType(URI.create("https://example.com/problems/invalid-parameter"));
61+
problemDetail.setTitle("Invalid Parameter");
62+
problemDetail.setInstance(URI.create(request.getRequestURI()));
63+
problemDetail.setProperty("timestamp", Instant.now());
64+
65+
return ResponseEntity.badRequest().body(problemDetail);
66+
}
67+
68+
/**
69+
* Handle RuntimeException for unexpected errors.
70+
* Returns HTTP 500 Internal Server Error with RFC 7807 Problem Details format.
71+
*
72+
* Does not expose sensitive internal error details to clients.
73+
*/
74+
@ExceptionHandler(RuntimeException.class)
75+
public ResponseEntity<ProblemDetail> handleRuntimeException(
76+
RuntimeException ex, HttpServletRequest request) {
77+
78+
String errorId = UUID.randomUUID().toString();
79+
logger.error("Unexpected runtime exception with ID: {}", errorId, ex);
80+
81+
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
82+
HttpStatus.INTERNAL_SERVER_ERROR,
83+
"An unexpected error occurred while processing the request"
84+
);
85+
86+
problemDetail.setType(URI.create("https://example.com/problems/internal-error"));
87+
problemDetail.setTitle("Internal Server Error");
88+
problemDetail.setInstance(URI.create(request.getRequestURI()));
89+
problemDetail.setProperty("timestamp", Instant.now());
90+
problemDetail.setProperty("errorId", errorId);
91+
92+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail);
93+
}
94+
95+
/**
96+
* Handle generic Exception for any unexpected errors not caught by specific handlers.
97+
* Returns HTTP 500 Internal Server Error with RFC 7807 Problem Details format.
98+
*/
99+
@ExceptionHandler(Exception.class)
100+
public ResponseEntity<ProblemDetail> handleGenericException(
101+
Exception ex, HttpServletRequest request) {
102+
103+
String errorId = UUID.randomUUID().toString();
104+
logger.error("Unexpected exception with ID: {}", errorId, ex);
105+
106+
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
107+
HttpStatus.INTERNAL_SERVER_ERROR,
108+
"An unexpected error occurred while processing the request"
109+
);
110+
111+
problemDetail.setType(URI.create("https://example.com/problems/internal-error"));
112+
problemDetail.setTitle("Internal Server Error");
113+
problemDetail.setInstance(URI.create(request.getRequestURI()));
114+
problemDetail.setProperty("timestamp", Instant.now());
115+
problemDetail.setProperty("errorId", errorId);
116+
117+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail);
118+
}
119+
}

0 commit comments

Comments
 (0)