-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFacialRecognitionController.java
More file actions
262 lines (232 loc) · 10.7 KB
/
Copy pathFacialRecognitionController.java
File metadata and controls
262 lines (232 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package com.crimeLink.analyzer.controller;
import com.crimeLink.analyzer.service.FacialRecognitionService;
import com.crimeLink.analyzer.util.LogSanitizer;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* REST Controller for Facial Recognition operations.
* Acts as API Gateway layer, routing requests to the Python ML microservice.
*
* Architecture Pattern: Hybrid Monolith + Microservices
* - Spring Boot handles authentication, authorization, and request routing
* - Python FastAPI handles ML inference (facial recognition)
*
* Endpoints:
* - POST /api/facial/analyze - Analyze suspect image for matches
* - POST /api/facial/register - Register new criminal face
* - GET /api/facial/criminals - List registered criminals
* - GET /api/facial/history - Get recognition history
* - GET /api/facial/health - Check ML service health
*/
@RestController
@RequestMapping("/api/facial")
@RequiredArgsConstructor
@Slf4j
public class FacialRecognitionController {
private final FacialRecognitionService facialRecognitionService;
/**
* Analyze a suspect image for facial recognition matches.
* Requires authentication - user ID is extracted from JWT token.
*
* @param image The image file to analyze (multipart)
* @param threshold Similarity threshold (0-100), default 45
* @param caseId Optional case ID for linking to investigation
* @return Analysis results with matched criminals
*/
@PostMapping("/analyze")
public ResponseEntity<?> analyzeImage(
@RequestParam("image") MultipartFile image,
@RequestParam(value = "threshold", required = false, defaultValue = "45") Float threshold,
@RequestParam(value = "case_id", required = false) String caseId) {
try {
// Get authenticated user ID from security context
String userId = getCurrentUserId();
log.info("Facial recognition analysis requested by user: {}", userId);
// Validate image
ResponseEntity<?> validationError = validateImageFile(image, 10 * 1024 * 1024); // 10MB
if (validationError != null) {
return validationError;
}
// Validate threshold range
if (threshold != null && (threshold < 0 || threshold > 100)) {
log.warn("Validation failed: Threshold {} out of range [0-100]", threshold);
return ResponseEntity.badRequest()
.body(Map.of("error", "Threshold must be between 0 and 100"));
}
// Forward to ML service
JsonNode result = facialRecognitionService.analyzeImage(image, threshold, userId, caseId);
return ResponseEntity.ok(result);
} catch (RuntimeException e) {
log.error("Facial recognition analysis failed: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
}
}
/**
* Register a new criminal with their photo for facial recognition.
* Requires authentication.
*
* @param photo Photo of the criminal
* @param criminalId Optional existing criminal ID to link
* @param name Criminal's name
* @param nic National ID Card number
* @param riskLevel Risk level (high, medium, low)
* @return Registration result with criminal details
*/
@PostMapping("/register")
public ResponseEntity<?> registerCriminal(
@RequestParam("photo") MultipartFile photo,
@RequestParam(value = "criminal_id", required = false) String criminalId,
@RequestParam("name") String name,
@RequestParam("nic") String nic,
@RequestParam(value = "risk_level", required = false, defaultValue = "medium") String riskLevel,
@RequestParam(value = "crime_history", required = false) String crimeHistory,
@RequestParam(value = "address", required = false) String address,
@RequestParam(value = "contact_number", required = false) String contactNumber,
@RequestParam(value = "secondary_contact", required = false) String secondaryContact,
@RequestParam(value = "date_of_birth", required = false) String dateOfBirth,
@RequestParam(value = "gender", required = false) String gender,
@RequestParam(value = "alias", required = false) String alias,
@RequestParam(value = "status", required = false, defaultValue = "active") String status) {
try {
log.info("Criminal registration requested: {} ({})", LogSanitizer.sanitize(name), LogSanitizer.sanitize(nic));
// Validate required text fields
ResponseEntity<?> nameValidation = validateRequiredText(name, "name");
if (nameValidation != null) {
return nameValidation;
}
ResponseEntity<?> nicValidation = validateRequiredText(nic, "nic");
if (nicValidation != null) {
return nicValidation;
}
// Validate photo
ResponseEntity<?> photoValidation = validateImageFile(photo, 10 * 1024 * 1024); // 10MB
if (photoValidation != null) {
return photoValidation;
}
// Forward to ML service
JsonNode result = facialRecognitionService.registerCriminal(
photo, criminalId, name, nic, riskLevel, crimeHistory,
address, contactNumber, secondaryContact, dateOfBirth, gender, alias, status);
return ResponseEntity.ok(result);
} catch (RuntimeException e) {
log.error("Criminal registration failed: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
}
}
/**
* Get list of all registered criminals with face embeddings.
*
* @return List of criminals
*/
@GetMapping("/criminals")
public ResponseEntity<?> getCriminals() {
try {
JsonNode result = facialRecognitionService.getCriminals();
return ResponseEntity.ok(result);
} catch (RuntimeException e) {
log.error("Failed to fetch criminals: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
}
}
/**
* Get facial recognition history/audit logs.
*
* @param limit Maximum number of records (default 50)
* @return Recognition history
*/
@GetMapping("/history")
public ResponseEntity<?> getRecognitionHistory(
@RequestParam(value = "limit", required = false, defaultValue = "50") Integer limit) {
try {
JsonNode result = facialRecognitionService.getRecognitionHistory(limit);
return ResponseEntity.ok(result);
} catch (RuntimeException e) {
log.error("Failed to fetch history: {}", e.getMessage());
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
}
}
/**
* Health check endpoint for the facial recognition ML service.
* Public endpoint for monitoring.
*
* @return Health status
*/
@GetMapping("/health")
public ResponseEntity<?> checkHealth() {
JsonNode health = facialRecognitionService.checkHealth();
String status = health.has("status") ? health.get("status").asText() : "unknown";
if ("healthy".equals(status)) {
return ResponseEntity.ok(health);
} else {
return ResponseEntity.status(503).body(health);
}
}
/**
* Validate image file for facial recognition.
* Checks: file not empty, content type is image/*, file size within limit.
*
* @param file The file to validate
* @param maxSizeBytes Maximum allowed file size in bytes
* @return ResponseEntity with error if validation fails, null if valid
*/
private ResponseEntity<?> validateImageFile(MultipartFile file, long maxSizeBytes) {
if (file == null || file.isEmpty()) {
log.warn("Validation failed: Empty image file");
return ResponseEntity.badRequest()
.body(Map.of("error", "No image provided"));
}
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
log.warn("Validation failed: Invalid content type '{}' for file '{}'",
LogSanitizer.sanitize(contentType), LogSanitizer.sanitize(file.getOriginalFilename()));
return ResponseEntity.badRequest()
.body(Map.of("error", "Invalid file type. Please upload an image."));
}
long fileSize = file.getSize();
if (fileSize > maxSizeBytes) {
log.warn("Validation failed: Image size {} exceeds limit {} for file '{}'",
fileSize, maxSizeBytes, LogSanitizer.sanitize(file.getOriginalFilename()));
return ResponseEntity.badRequest()
.body(Map.of("error", "File size exceeds maximum limit of " +
(maxSizeBytes / (1024 * 1024)) + "MB: " + file.getOriginalFilename()));
}
return null; // Validation passed
}
/**
* Validate required text field.
* Checks: not null, not blank after trimming.
*
* @param value The value to validate
* @param fieldName Name of the field (for error message)
* @return ResponseEntity with error if validation fails, null if valid
*/
private ResponseEntity<?> validateRequiredText(String value, String fieldName) {
if (value == null || value.trim().isEmpty()) {
log.warn("Validation failed: Required field '{}' is missing or empty", fieldName);
return ResponseEntity.badRequest()
.body(Map.of("error", fieldName + " is required"));
}
return null; // Validation passed
}
/**
* Extract current user ID from security context.
*/
private String getCurrentUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) {
return auth.getName();
}
return "unknown";
}
}