Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pom-dependency-tree.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ai.elimu:webapp:war:2.6.44-SNAPSHOT
+- ai.elimu:model:jar:model-2.0.97:compile
| \- com.google.code.gson:gson:jar:2.13.0:compile
| \- com.google.errorprone:error_prone_annotations:jar:2.37.0:compile
ai.elimu:webapp:war:2.6.47-SNAPSHOT
+- ai.elimu:model:jar:model-2.0.111:compile
| \- com.google.code.gson:gson:jar:2.13.1:compile
| \- com.google.errorprone:error_prone_annotations:jar:2.38.0:compile
+- org.springframework:spring-context:jar:6.0.9:compile
| +- org.springframework:spring-aop:jar:6.0.9:compile
| +- org.springframework:spring-beans:jar:6.0.9:compile
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<model.version>2.0.97</model.version>
<model.version>2.0.111</model.version>
<hibernate.version>6.1.7.Final</hibernate.version>
<jetty.version>11.0.24</jetty.version>
<spring.version>6.0.9</spring.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package ai.elimu.dao;

import java.util.Calendar;
import java.util.List;

import org.springframework.dao.DataAccessException;

import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;

public interface LetterSoundAssessmentEventDao extends GenericDao<LetterSoundAssessmentEvent> {

LetterSoundAssessmentEvent read(Calendar timestamp, String androidId, String packageName) throws DataAccessException;

List<LetterSoundAssessmentEvent> readAll(String androidId) throws DataAccessException;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
package ai.elimu.dao.jpa;

import java.util.Calendar;
import java.util.List;

import org.springframework.dao.DataAccessException;

import ai.elimu.dao.LetterSoundAssessmentEventDao;
import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;
import jakarta.persistence.NoResultException;

public class LetterSoundAssessmentEventDaoJpa extends GenericDaoJpa<LetterSoundAssessmentEvent> implements LetterSoundAssessmentEventDao {

@Override
public LetterSoundAssessmentEvent read(Calendar timestamp, String androidId, String packageName) throws DataAccessException {
try {
return (LetterSoundAssessmentEvent) em.createQuery(
"SELECT event " +
"FROM LetterSoundAssessmentEvent event " +
"WHERE event.timestamp = :timestamp " +
"AND event.androidId = :androidId " +
"AND event.packageName = :packageName")
.setParameter("timestamp", timestamp)
.setParameter("androidId", androidId)
.setParameter("packageName", packageName)
.getSingleResult();
} catch (NoResultException e) {
logger.info("LetterSoundAssessmentEvent (" + timestamp.getTimeInMillis() + ", " + androidId + ", \"" + packageName + "\") was not found");
return null;
}
}

@Override
public List<LetterSoundAssessmentEvent> readAll(String androidId) throws DataAccessException {
return em.createQuery(
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,23 @@ public abstract class AssessmentEvent extends BaseEntity {
@ManyToOne
private Application application;

/**
* A value in the range [0.0, 1.0].
*/
private Float masteryScore;

/**
* The number of milliseconds passed between the student opening the assessment task
* and submitting a response. E.g. <code>15000</code>.
*/
private Long timeSpentMs;

/**
* Any additional data should be stored in the format of a JSON object.
*
* Example:
* <pre>
* {'word_ids_presented': [1,2,3], 'word_id_selected': 2}
* {'word_ids_presented': [1,2,3], 'word_id_selected': [2]}
* </pre>
*/
@Column(length = 1024)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai.elimu.entity.analytics;

import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

Expand All @@ -12,11 +13,13 @@ public class LetterSoundAssessmentEvent extends AssessmentEvent {
/**
* The sequence of letters. E.g. <code>"sh"</code>.
*/
@NotNull
private String letterSoundLetters;

/**
* The sequence of sounds (IPA values). E.g. <code>"ʃ"</code>.
*/
@NotNull
private String letterSoundSounds;

/**
Expand All @@ -25,15 +28,4 @@ public class LetterSoundAssessmentEvent extends AssessmentEvent {
* In this case, the {@link #letterSoundId} will be {@code null}.
*/
private Long letterSoundId;

/**
* A value in the range [0.0, 1.0].
*/
private Float masteryScore;

/**
* The number of milliseconds passed between the student opening the assessment task
* and submitting a response. E.g. <code>15000</code>.
*/
private Long timeSpentMs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,4 @@ public class WordAssessmentEvent extends AssessmentEvent {
* In this case, the {@link #wordId} will be {@code null}.
*/
private Long wordId;

/**
* A value in the range [0.0, 1.0].
*/
private Float masteryScore;

/**
* The number of milliseconds passed between the student opening the assessment task
* and submitting a response. E.g. <code>15000</code>.
*/
private Long timeSpentMs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ai.elimu.tasks.analytics;

import ai.elimu.dao.StudentDao;
import ai.elimu.dao.LetterSoundAssessmentEventDao;
import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;
import ai.elimu.entity.analytics.students.Student;
import ai.elimu.model.v2.enums.Language;
import ai.elimu.rest.v2.analytics.LetterSoundAssessmentEventsRestController;
import ai.elimu.util.ConfigHelper;
import ai.elimu.util.csv.CsvAnalyticsExtractionHelper;
import java.io.File;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
* Extracts assessment events from CSV files previously received by the {@link LetterSoundAssessmentEventsRestController}, and imports them into the database.
* <p/>
* <p>
* Expected folder structure:
* <pre>
* ├── lang-ENG
* │   ├── analytics
* │   │   ├── android-id-e387e38700000001
* │   │   │   └── version-code-3001018
* │   │   │   └── letter-sound-assessment-events
* │   │   │   ├── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-09.csv
* │   │   │   ├── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-10.csv
* │   │   │   ├── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-11.csv
* │   │   │   ├── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-14.csv
* │   │   │   ├── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-18.csv
* │   │   │   └── e387e38700000001_3001018_letter-sound-assessment-events_2024-10-20.csv
* │   │   ├── android-id-e387e38700000002
* │   │   │   └── version-code-3001018
* │   │   │   └── letter-sound-assessment-events
* │   │   │   ├── e387e38700000002_3001018_letter-sound-assessment-events_2024-10-09.csv
* │   │   │   ├── e387e38700000002_3001018_letter-sound-assessment-events_2024-10-10.csv
* │   │   │   ├── e387e38700000002_3001018_letter-sound-assessment-events_2024-10-11.csv
* </pre>
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LetterSoundAssessmentEventImportScheduler {

private final LetterSoundAssessmentEventDao letterSoundAssessmentEventDao;

private final StudentDao studentDao;

@Scheduled(cron = "00 15 * * * *") // 15 minutes past every hour
public synchronized void execute() {
log.info("execute");

// Lookup CSV files stored 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");
log.info("analyticsDir: " + analyticsDir);
analyticsDir.mkdirs();
for (File analyticsDirFile : analyticsDir.listFiles()) {
if (analyticsDirFile.getName().startsWith("android-id-")) {
File androidIdDir = new File(analyticsDir, analyticsDirFile.getName());
for (File androidIdDirFile : androidIdDir.listFiles()) {
if (androidIdDirFile.getName().startsWith("version-code-")) {
File versionCodeDir = new File(androidIdDir, androidIdDirFile.getName());
for (File versionCodeDirFile : versionCodeDir.listFiles()) {
if (versionCodeDirFile.getName().equals("letter-sound-assessment-events")) {
File letterSoundAssessmentEventsDir = new File(versionCodeDir, versionCodeDirFile.getName());
for (File csvFile : letterSoundAssessmentEventsDir.listFiles()) {
log.info("csvFile: " + csvFile);

// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify CSV extraction helper bug fix.

Based on the relevant code snippet, there's a bug in the CSV extraction helper where letterSoundSounds is incorrectly assigned to letterSoundLetters.


🏁 Script executed:

#!/bin/bash
# Description: Check if the CSV extraction helper has the setter bug
# Expected: Find incorrect assignment of letterSoundSounds to letterSoundLetters

rg -A 2 -B 2 "setLetterSoundLetters.*letterSoundSounds" src/main/java/ai/elimu/util/csv/CsvAnalyticsExtractionHelper.java

Length of output: 376


Fix CSV helper setter bug

The CsvAnalyticsExtractionHelper is incorrectly assigning the “sounds” value to the “letters” field. In src/main/java/ai/elimu/util/csv/CsvAnalyticsExtractionHelper.java, update the setter call:

@@ -XXX,3 +XXX,3 @@
-    letterSoundAssessmentEvent.setLetterSoundLetters(letterSoundSounds);
+    letterSoundAssessmentEvent.setLetterSoundSounds(letterSoundSounds);

This ensures that letterSoundSounds is stored in the proper field.

🤖 Prompt for AI Agents
In src/main/java/ai/elimu/util/csv/CsvAnalyticsExtractionHelper.java around the
setter calls for letterSoundLetters and letterSoundSounds, there is a bug where
letterSoundSounds is incorrectly assigned to letterSoundLetters. To fix this,
locate the setter call that assigns letterSoundSounds and ensure it is set using
the correct setter method setLetterSoundSounds instead of setLetterSoundLetters.
This will correctly store the sounds value in the appropriate field.

log.info("events.size(): " + events.size());

// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent = letterSoundAssessmentEventDao.read(event.getTimestamp(), event.getAndroidId(), event.getPackageName());
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}

// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}

// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
Comment on lines +74 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add transaction management for data consistency.

The import process lacks transaction boundaries, which could lead to inconsistent state if the process fails partway through. Student creation and event insertion should be wrapped in transactions to ensure atomicity.

Add @Transactional annotation and proper transaction boundaries:

+import org.springframework.transaction.annotation.Transactional;

 @Service
 @RequiredArgsConstructor
 @Slf4j
 public class LetterSoundAssessmentEventImportScheduler {

   @Scheduled(cron = "00 15 * * * *") // 15 minutes past every hour
+  @Transactional
   public void execute() {

Consider wrapping the inner processing loop in separate transactions to limit rollback scope.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent = letterSoundAssessmentEventDao.read(event.getTimestamp(), event.getAndroidId(), event.getPackageName());
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}
// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}
// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class LetterSoundAssessmentEventImportScheduler {
@Scheduled(cron = "00 15 * * * *") // 15 minutes past every hour
@Transactional
public void execute() {
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent =
letterSoundAssessmentEventDao.read(
event.getTimestamp(), event.getAndroidId(), event.getPackageName());
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}
// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}
// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
}
}
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/tasks/analytics/LetterSoundAssessmentEventImportScheduler.java
around lines 74 to 99, the import process lacks transaction management, risking
inconsistent database state if failures occur mid-process. To fix this, add the
@Transactional annotation to the method handling this import or explicitly
manage transactions around the student creation and event insertion logic.
Consider wrapping each iteration of the loop in its own transaction to limit
rollback scope and ensure atomicity for each event processed.

🛠️ Refactor suggestion

Add error handling for CSV processing and database operations.

The code lacks error handling around CSV extraction and database operations. If CSV parsing fails or database operations throw exceptions, the scheduler could fail silently or crash.

Wrap file processing in try-catch blocks:

 for (File csvFile : letterSoundAssessmentEventsDir.listFiles()) {
   log.info("csvFile: " + csvFile);
-
-  // Convert from CSV to Java
-  List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
-  log.info("events.size(): " + events.size());
-
-  // Store in database
-  for (LetterSoundAssessmentEvent event : events) {
+  
+  try {
+    // Convert from CSV to Java
+    List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
+    log.info("events.size(): " + events.size());
+
+    // Store in database
+    for (LetterSoundAssessmentEvent event : events) {
       // ... existing processing logic
+    }
+  } catch (Exception e) {
+    log.error("Failed to process CSV file: " + csvFile.getName(), e);
+    // Consider moving file to error directory or marking as failed
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent = letterSoundAssessmentEventDao.read(event.getTimestamp(), event.getAndroidId(), event.getPackageName());
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}
// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}
// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
for (File csvFile : letterSoundAssessmentEventsDir.listFiles()) {
log.info("csvFile: " + csvFile);
try {
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent = letterSoundAssessmentEventDao.read(
event.getTimestamp(),
event.getAndroidId(),
event.getPackageName()
);
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}
// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}
// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
} catch (Exception e) {
log.error("Failed to process CSV file: " + csvFile.getName(), e);
// Consider moving the file to an error directory or marking it as failed
}
}
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/tasks/analytics/LetterSoundAssessmentEventImportScheduler.java
around lines 74 to 99, add try-catch blocks around the CSV extraction and
database operations to handle potential exceptions. Wrap the call to
CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents and the
subsequent database read and create calls in try-catch blocks to catch and log
exceptions, preventing the scheduler from failing silently or crashing. Ensure
that errors are logged with sufficient detail for debugging.

}
}
}
}
}
}
}
Comment on lines +62 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Add null checks and error handling for directory traversal.

The directory traversal logic is vulnerable to NullPointerException if any directory returns null from listFiles(). This can happen if the directory doesn't exist, lacks read permissions, or encounters I/O errors.

 for (File analyticsDirFile : analyticsDir.listFiles()) {
+  if (analyticsDirFile == null) continue;
   if (analyticsDirFile.getName().startsWith("android-id-")) {
     File androidIdDir = new File(analyticsDir, analyticsDirFile.getName());
-    for (File androidIdDirFile : androidIdDir.listFiles()) {
+    File[] androidIdFiles = androidIdDir.listFiles();
+    if (androidIdFiles == null) continue;
+    for (File androidIdDirFile : androidIdFiles) {
       if (androidIdDirFile.getName().startsWith("version-code-")) {
         File versionCodeDir = new File(androidIdDir, androidIdDirFile.getName());
-        for (File versionCodeDirFile : versionCodeDir.listFiles()) {
+        File[] versionCodeFiles = versionCodeDir.listFiles();
+        if (versionCodeFiles == null) continue;
+        for (File versionCodeDirFile : versionCodeFiles) {
           if (versionCodeDirFile.getName().equals("letter-sound-assessment-events")) {
             File letterSoundAssessmentEventsDir = new File(versionCodeDir, versionCodeDirFile.getName());
-            for (File csvFile : letterSoundAssessmentEventsDir.listFiles()) {
+            File[] csvFiles = letterSoundAssessmentEventsDir.listFiles();
+            if (csvFiles == null) continue;
+            for (File csvFile : csvFiles) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (File analyticsDirFile : analyticsDir.listFiles()) {
if (analyticsDirFile.getName().startsWith("android-id-")) {
File androidIdDir = new File(analyticsDir, analyticsDirFile.getName());
for (File androidIdDirFile : androidIdDir.listFiles()) {
if (androidIdDirFile.getName().startsWith("version-code-")) {
File versionCodeDir = new File(androidIdDir, androidIdDirFile.getName());
for (File versionCodeDirFile : versionCodeDir.listFiles()) {
if (versionCodeDirFile.getName().equals("letter-sound-assessment-events")) {
File letterSoundAssessmentEventsDir = new File(versionCodeDir, versionCodeDirFile.getName());
for (File csvFile : letterSoundAssessmentEventsDir.listFiles()) {
log.info("csvFile: " + csvFile);
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events = CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// Check if the event has already been stored in the database
LetterSoundAssessmentEvent existingLetterSoundAssessmentEvent = letterSoundAssessmentEventDao.read(event.getTimestamp(), event.getAndroidId(), event.getPackageName());
if (existingLetterSoundAssessmentEvent != null) {
log.warn("The event has already been stored in the database. Skipping data import.");
continue;
}
// Generate Student ID
Student existingStudent = studentDao.read(event.getAndroidId());
if (existingStudent == null) {
Student student = new Student();
student.setAndroidId(event.getAndroidId());
studentDao.create(student);
log.info("Stored Student in database with ID " + student.getId());
}
// Store the event in the database
letterSoundAssessmentEventDao.create(event);
log.info("Stored event in database with ID " + event.getId());
}
}
}
}
}
}
}
}
for (File analyticsDirFile : analyticsDir.listFiles()) {
if (analyticsDirFile == null) {
continue;
}
if (analyticsDirFile.getName().startsWith("android-id-")) {
File androidIdDir = new File(analyticsDir, analyticsDirFile.getName());
File[] androidIdFiles = androidIdDir.listFiles();
if (androidIdFiles == null) {
continue;
}
for (File androidIdDirFile : androidIdFiles) {
if (androidIdDirFile.getName().startsWith("version-code-")) {
File versionCodeDir = new File(androidIdDir, androidIdDirFile.getName());
File[] versionCodeFiles = versionCodeDir.listFiles();
if (versionCodeFiles == null) {
continue;
}
for (File versionCodeDirFile : versionCodeFiles) {
if (versionCodeDirFile.getName().equals("letter-sound-assessment-events")) {
File letterSoundAssessmentEventsDir = new File(versionCodeDir, versionCodeDirFile.getName());
File[] csvFiles = letterSoundAssessmentEventsDir.listFiles();
if (csvFiles == null) {
continue;
}
for (File csvFile : csvFiles) {
log.info("csvFile: " + csvFile);
// Convert from CSV to Java
List<LetterSoundAssessmentEvent> events =
CsvAnalyticsExtractionHelper.extractLetterSoundAssessmentEvents(csvFile);
log.info("events.size(): " + events.size());
// Store in database
for (LetterSoundAssessmentEvent event : events) {
// ...
}
}
}
}
}
}
}
}
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/tasks/analytics/LetterSoundAssessmentEventImportScheduler.java
between lines 62 and 106, add null checks after each call to listFiles() to
ensure the returned array is not null before iterating. If listFiles() returns
null, skip processing that directory to prevent NullPointerException.
Additionally, consider adding error handling or logging to capture cases where
directories are missing or inaccessible.


log.info("execute complete");
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ai.elimu.util.csv;

import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;
import ai.elimu.entity.analytics.LetterSoundLearningEvent;
import ai.elimu.entity.analytics.StoryBookLearningEvent;
import ai.elimu.entity.analytics.VideoLearningEvent;
Expand Down Expand Up @@ -30,6 +31,68 @@
@Slf4j
public class CsvAnalyticsExtractionHelper {

public static List<LetterSoundAssessmentEvent> extractLetterSoundAssessmentEvents(File csvFile) {
log.info("extractLetterSoundAssessmentEvents");

Integer versionCode = AnalyticsHelper.extractVersionCodeFromCsvFilename(csvFile.getName());
log.info("versionCode: " + versionCode);

List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = new ArrayList<>();

// Iterate each row in the CSV file
Path csvFilePath = Paths.get(csvFile.toURI());
log.info("csvFilePath: " + csvFilePath);
try {
Reader reader = Files.newBufferedReader(csvFilePath);
CSVFormat csvFormat = CSVFormat.DEFAULT.withFirstRecordAsHeader();
log.info("header: " + Arrays.toString(csvFormat.getHeader()));
CSVParser csvParser = new CSVParser(reader, csvFormat);
for (CSVRecord csvRecord : csvParser) {
log.info("csvRecord: " + csvRecord);

// Convert from CSV to Java

LetterSoundAssessmentEvent letterSoundAssessmentEvent = new LetterSoundAssessmentEvent();

long timestampInMillis = Long.valueOf(csvRecord.get("timestamp"));
Calendar timestamp = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
timestamp.setTimeInMillis(timestampInMillis);
letterSoundAssessmentEvent.setTimestamp(timestamp);

String androidId = AnalyticsHelper.extractAndroidIdFromCsvFilename(csvFile.getName());
letterSoundAssessmentEvent.setAndroidId(androidId);

String packageName = csvRecord.get("package_name");
letterSoundAssessmentEvent.setPackageName(packageName);

Float masteryScore = Float.valueOf(csvRecord.get("mastery_score"));
letterSoundAssessmentEvent.setMasteryScore(masteryScore);

Long timeSpentMs = Long.valueOf(csvRecord.get("time_spent_ms"));
letterSoundAssessmentEvent.setTimeSpentMs(timeSpentMs);

String additionalData = csvRecord.get("additional_data");
letterSoundAssessmentEvent.setAdditionalData(additionalData);

String letterSoundLetters = csvRecord.get("letter_sound_letters");
letterSoundAssessmentEvent.setLetterSoundLetters(letterSoundLetters);

String letterSoundSounds = csvRecord.get("letter_sound_sounds");
letterSoundAssessmentEvent.setLetterSoundLetters(letterSoundSounds);

Long letterSoundId = Long.valueOf(csvRecord.get("letter_sound_id"));
letterSoundAssessmentEvent.setLetterSoundId(letterSoundId);

letterSoundAssessmentEvents.add(letterSoundAssessmentEvent);
}
csvParser.close();
} catch (IOException ex) {
log.error(ex.getMessage());
}

return letterSoundAssessmentEvents;
}

public static List<LetterSoundLearningEvent> extractLetterSoundLearningEvents(File csvFile) {
log.info("extractLetterSoundLearningEvents");

Expand Down Expand Up @@ -125,12 +188,6 @@ public static List<WordAssessmentEvent> extractWordAssessmentEvents(File csvFile
String packageName = csvRecord.get("package_name");
wordAssessmentEvent.setPackageName(packageName);

String wordText = csvRecord.get("word_text");
wordAssessmentEvent.setWordText(wordText);

Long wordId = Long.valueOf(csvRecord.get("word_id"));
wordAssessmentEvent.setWordId(wordId);

Float masteryScore = Float.valueOf(csvRecord.get("mastery_score"));
wordAssessmentEvent.setMasteryScore(masteryScore);

Expand All @@ -140,6 +197,12 @@ public static List<WordAssessmentEvent> extractWordAssessmentEvents(File csvFile
// String additionalData = csvRecord.get("additional_data");
// wordAssessmentEvent.setAdditionalData(additionalData);

String wordText = csvRecord.get("word_text");
wordAssessmentEvent.setWordText(wordText);

Long wordId = Long.valueOf(csvRecord.get("word_id"));
wordAssessmentEvent.setWordId(wordId);

wordAssessmentEvents.add(wordAssessmentEvent);
}
csvParser.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ public void handleRequest(
"id",
"timestamp",
"package_name",
"letter_sound_letters",
"letter_sound_sounds",
"letter_sound_id",
"mastery_score",
"time_spent_ms",
"additional_data"
"additional_data",
"letter_sound_letters",
"letter_sound_sounds",
"letter_sound_id"
)
.build();

Expand All @@ -66,12 +66,12 @@ public void handleRequest(
letterSoundAssessmentEvent.getId(),
letterSoundAssessmentEvent.getTimestamp().getTimeInMillis(),
letterSoundAssessmentEvent.getPackageName(),
letterSoundAssessmentEvent.getLetterSoundLetters(),
letterSoundAssessmentEvent.getLetterSoundSounds(),
letterSoundAssessmentEvent.getLetterSoundId(),
letterSoundAssessmentEvent.getMasteryScore(),
letterSoundAssessmentEvent.getTimeSpentMs(),
letterSoundAssessmentEvent.getAdditionalData()
letterSoundAssessmentEvent.getAdditionalData(),
letterSoundAssessmentEvent.getLetterSoundLetters(),
letterSoundAssessmentEvent.getLetterSoundSounds(),
letterSoundAssessmentEvent.getLetterSoundId()
);
}
csvPrinter.flush();
Expand Down
Loading