-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(rest): video learning events #1904
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package ai.elimu.rest.v2.analytics; | ||
|
||
import ai.elimu.model.v2.enums.Language; | ||
import ai.elimu.util.AnalyticsHelper; | ||
import ai.elimu.util.ConfigHelper; | ||
import java.io.File; | ||
import javax.servlet.http.HttpServletResponse; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.json.JSONObject; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestMethod; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
@RestController | ||
@RequestMapping(value = "/rest/v2/analytics/video-learning-events", produces = MediaType.APPLICATION_JSON_VALUE) | ||
public class VideoLearningEventsRestController { | ||
|
||
private Logger logger = LogManager.getLogger(); | ||
|
||
@RequestMapping(value = "/csv", method = RequestMethod.POST) | ||
public String handleUploadCsvRequest( | ||
@RequestParam("file") MultipartFile multipartFile, | ||
HttpServletResponse response | ||
) { | ||
logger.info("handleUploadCsvRequest"); | ||
|
||
JSONObject jsonResponseObject = new JSONObject(); | ||
try { | ||
String contentType = multipartFile.getContentType(); | ||
logger.info("contentType: " + contentType); | ||
|
||
long size = multipartFile.getSize(); | ||
logger.info("size: " + size); | ||
if (size == 0) { | ||
throw new IllegalArgumentException("Empty file"); | ||
} | ||
|
||
// Expected format: "7161a85a0e4751cd_3001012_video-learning-events_2020-04-23.csv" | ||
String originalFilename = multipartFile.getOriginalFilename(); | ||
logger.info("originalFilename: " + originalFilename); | ||
if (originalFilename.length() != 61) { | ||
throw new IllegalArgumentException("Unexpected filename"); | ||
} | ||
|
||
String androidIdExtractedFromFilename = AnalyticsHelper.extractAndroidIdFromCsvFilename(originalFilename); | ||
logger.info("androidIdExtractedFromFilename: \"" + androidIdExtractedFromFilename + "\""); | ||
|
||
Integer versionCodeExtractedFromFilename = AnalyticsHelper.extractVersionCodeFromCsvFilename(originalFilename); | ||
logger.info("versionCodeExtractedFromFilename: " + versionCodeExtractedFromFilename); | ||
Comment on lines
+34
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance input validation and error handling. While the current validation is good, consider the following improvements:
Here's a suggested refactor: if (!"text/csv".equals(contentType)) {
throw new IllegalArgumentException("Invalid file type. Expected CSV.");
}
if (size == 0) {
throw new IllegalArgumentException("File is empty.");
}
if (originalFilename == null || originalFilename.length() != 61) {
throw new IllegalArgumentException("Invalid filename format.");
}
// Consider creating a custom exception for more granular error handling
class FileValidationException extends RuntimeException {
public FileValidationException(String message) {
super(message);
}
}
// Use the custom exception in your validation logic
if (condition) {
throw new FileValidationException("Specific error message");
} This approach provides more informative error messages and allows for more granular error handling. |
||
|
||
byte[] bytes = multipartFile.getBytes(); | ||
logger.info("bytes.length: " + bytes.length); | ||
|
||
// Store the original CSV file on the filesystem | ||
File elimuAiDir = new File(System.getProperty("user.home"), ".elimu-ai"); | ||
File languageDir = new File(elimuAiDir, "lang-" + Language.valueOf(ConfigHelper.getProperty("content.language"))); | ||
File analyticsDir = new File(languageDir, "analytics"); | ||
File androidIdDir = new File(analyticsDir, "android-id-" + androidIdExtractedFromFilename); | ||
File versionCodeDir = new File(androidIdDir, "version-code-" + versionCodeExtractedFromFilename); | ||
File videoLearningEventsDir = new File(versionCodeDir, "video-learning-events"); | ||
videoLearningEventsDir.mkdirs(); | ||
File csvFile = new File(videoLearningEventsDir, originalFilename); | ||
logger.info("Storing CSV file at " + csvFile); | ||
multipartFile.transferTo(csvFile); | ||
jo-elimu marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+56
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling for file storage and consider externalizing the storage configuration. The current implementation has a few areas for improvement:
Here's a suggested refactor: // Externalize this configuration
@Value("${app.storage.root:/tmp/.elimu-ai}")
private String storageRoot;
// In the method
File storageDir = new File(storageRoot,
String.format("lang-%s/analytics/android-id-%s/version-code-%d/video-learning-events",
Language.valueOf(ConfigHelper.getProperty("content.language")),
androidIdExtractedFromFilename,
versionCodeExtractedFromFilename));
if (!storageDir.exists() && !storageDir.mkdirs()) {
throw new IOException("Failed to create directory: " + storageDir);
}
File csvFile = new File(storageDir, originalFilename);
logger.info("Storing CSV file at " + csvFile);
try {
multipartFile.transferTo(csvFile);
} catch (IOException e) {
throw new IOException("Failed to save file: " + csvFile, e);
} This approach provides better error handling and allows for easier configuration of the storage location across different environments. |
||
|
||
jsonResponseObject.put("result", "success"); | ||
jsonResponseObject.put("successMessage", "The CSV file was uploaded"); | ||
response.setStatus(HttpStatus.OK.value()); | ||
} catch (Exception ex) { | ||
logger.error(ex); | ||
|
||
jsonResponseObject.put("result", "error"); | ||
jsonResponseObject.put("errorMessage", ex.getMessage()); | ||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); | ||
} | ||
|
||
String jsonResponse = jsonResponseObject.toString(); | ||
logger.info("jsonResponse: " + jsonResponse); | ||
return jsonResponse; | ||
Comment on lines
+71
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance response handling and logging practices. The current implementation of response handling is functional but can be improved:
Here's a suggested refactor using ResponseEntity: @RequestMapping(value = "/csv", method = RequestMethod.POST)
public ResponseEntity<String> handleUploadCsvRequest(@RequestParam("file") MultipartFile multipartFile) {
try {
// ... (existing file processing logic) ...
JSONObject jsonResponseObject = new JSONObject();
jsonResponseObject.put("result", "success");
jsonResponseObject.put("successMessage", "The CSV file was uploaded");
return ResponseEntity.ok(jsonResponseObject.toString());
} catch (Exception ex) {
logger.error("Error processing file upload", ex);
JSONObject jsonResponseObject = new JSONObject();
jsonResponseObject.put("result", "error");
jsonResponseObject.put("errorMessage", "An error occurred while processing the file");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(jsonResponseObject.toString());
}
} This approach eliminates the need for manually setting response status and provides a more Spring-like way of handling responses. For logging, consider logging only non-sensitive parts of the response: logger.info("Response status: " + jsonResponseObject.getString("result")); |
||
} | ||
Comment on lines
+71
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance response handling and logging practices. Consider the following improvements:
Here's a suggested refactor: try {
// Existing logic...
JSONObject successResponse = new JSONObject()
.put("result", "success")
.put("message", "The CSV file was uploaded successfully");
logger.info("File upload successful: " + originalFilename);
return ResponseEntity.ok(successResponse.toString());
} catch (IllegalArgumentException e) {
logger.warn("Invalid input: " + e.getMessage());
JSONObject errorResponse = new JSONObject()
.put("result", "error")
.put("message", "Invalid input: " + e.getMessage());
return ResponseEntity.badRequest().body(errorResponse.toString());
} catch (IOException e) {
logger.error("File processing error", e);
JSONObject errorResponse = new JSONObject()
.put("result", "error")
.put("message", "An error occurred while processing the file");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse.toString());
} catch (Exception e) {
logger.error("Unexpected error", e);
JSONObject errorResponse = new JSONObject()
.put("result", "error")
.put("message", "An unexpected error occurred");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse.toString());
} This approach provides more specific error handling, uses ResponseEntity for consistent response generation, and avoids logging potentially sensitive information. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package ai.elimu.rest.v2.analytics; | ||
|
||
import org.json.JSONObject; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.MockitoAnnotations; | ||
import org.springframework.mock.web.MockMultipartFile; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
import javax.servlet.http.HttpServletResponse; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.mockito.Mockito.*; | ||
|
||
public class VideoLearningEventsRestControllerTest { | ||
|
||
@InjectMocks | ||
private VideoLearningEventsRestController videoLearningEventsRestController; | ||
|
||
@Mock | ||
private HttpServletResponse response; | ||
|
||
@BeforeEach | ||
public void setUp() { | ||
MockitoAnnotations.openMocks(this); | ||
} | ||
|
||
@Test | ||
public void testHandleUploadCsvRequest_invalidFilename() { | ||
MultipartFile multipartFile = new MockMultipartFile( | ||
"file", | ||
"invalid_filename.csv", | ||
"text/csv", | ||
"test content".getBytes() | ||
); | ||
String jsonResponse = videoLearningEventsRestController.handleUploadCsvRequest(multipartFile, response); | ||
JSONObject jsonObject = new JSONObject(jsonResponse); | ||
assertEquals("error", jsonObject.getString("result")); | ||
assertEquals("Unexpected filename", jsonObject.getString("errorMessage")); | ||
verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | ||
} | ||
|
||
@Test | ||
public void testHandleUploadCsvRequest_emptyFile() { | ||
MultipartFile multipartFile = new MockMultipartFile( | ||
"file", | ||
"7161a85a0e4751cd_3001012_video-learning-events_2020-04-23.csv", | ||
"text/csv", | ||
"".getBytes() | ||
); | ||
String jsonResponse = videoLearningEventsRestController.handleUploadCsvRequest(multipartFile, response); | ||
JSONObject jsonObject = new JSONObject(jsonResponse); | ||
assertEquals("error", jsonObject.getString("result")); | ||
assertEquals("Empty file", jsonObject.getString("errorMessage")); | ||
verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider using ResponseEntity for improved response handling.
While the current implementation works, using Spring's ResponseEntity can provide more idiomatic and flexible response handling. It allows for easier manipulation of response headers, status codes, and body in a type-safe manner.
Here's a suggested refactor for the method signature:
This approach eliminates the need for manually setting response status and provides a more Spring-like way of handling responses.