CATMEController.java
package edu.ucsb.cs156.frontiers.controllers;
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "CATME")
@RequestMapping("/api/catme")
@RestController
public class CATMEController extends ApiController {
@Autowired private RosterStudentRepository rosterStudentRepository;
@Operation(summary = "Convert CATME roster names into course emails")
@PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
@PostMapping(value = "/emails", consumes = "text/plain", produces = "text/plain")
public String getCourseEmailsFromCatme(
@Parameter(name = "courseId") @RequestParam Long courseId,
@Parameter(name = "payload") @RequestBody(required = false) String payload) {
String normalizedPayload = payload == null ? "" : payload;
if (normalizedPayload.isBlank()) {
return "";
}
Map<String, String> emailByName =
StreamSupport.stream(rosterStudentRepository.findByCourseId(courseId).spliterator(), false)
.filter(
student ->
student.getFirstName() != null
&& student.getLastName() != null
&& student.getEmail() != null)
.collect(
Collectors.toMap(
student -> toLookupKey(student.getLastName(), student.getFirstName()),
RosterStudent::getEmail,
(first, second) -> first));
// \\R handles all line endings and -1 preserves trailing blank lines.
return Arrays.stream(normalizedPayload.split("\\R", -1))
.map(line -> formatEmailResultLine(extractName(line), emailByName))
.collect(Collectors.joining("\n"));
}
private static String extractName(String line) {
String trimmed = normalizeSpaces(line);
if (trimmed.isBlank()) {
return "";
}
// CATME lines look like: "LAST, FIRST MIDDLE <score> <timestamp>".
int dateStart = findDateTokenStart(trimmed);
if (dateStart == -1) {
return trimmed;
}
String beforeDate = trimmed.substring(0, dateStart).trim();
int lastSpace = beforeDate.lastIndexOf(' ');
if (lastSpace == -1) {
return beforeDate;
}
String possibleScore = beforeDate.substring(lastSpace + 1);
if (possibleScore.chars().allMatch(Character::isDigit)) {
return beforeDate.substring(0, lastSpace).trim();
}
return beforeDate;
}
private static String formatEmailResultLine(String name, Map<String, String> emailByName) {
if (name.isBlank()) {
return "";
}
String email = emailByName.get(toLookupKey(name));
if (email != null) {
return email;
}
return "# NO EMAIL FOUND FOR " + name;
}
private static String toLookupKey(String lastName, String firstName) {
return toLookupKey(lastName + ", " + firstName);
}
private static String toLookupKey(String name) {
return normalizeSpaces(name).toUpperCase().replaceFirst("\\s*,\\s*", ",");
}
private static String normalizeSpaces(String value) {
return value.replaceAll("\\s+", " ").trim();
}
private static int findDateTokenStart(String value) {
for (int i = 0; i + 10 <= value.length(); i++) {
if (isDateToken(value, i)) {
return i;
}
}
return -1;
}
private static boolean isDateToken(String value, int start) {
if (start > 0 && value.charAt(start - 1) != ' ') {
return false;
}
return Character.isDigit(value.charAt(start))
&& Character.isDigit(value.charAt(start + 1))
&& Character.isDigit(value.charAt(start + 2))
&& Character.isDigit(value.charAt(start + 3))
&& value.charAt(start + 4) == '-'
&& Character.isDigit(value.charAt(start + 5))
&& Character.isDigit(value.charAt(start + 6))
&& value.charAt(start + 7) == '-'
&& Character.isDigit(value.charAt(start + 8))
&& Character.isDigit(value.charAt(start + 9));
}
}