| 1 | package edu.ucsb.cs156.courses.services; | |
| 2 | ||
| 3 | import com.opencsv.CSVReader; | |
| 4 | import com.opencsv.CSVReaderBuilder; | |
| 5 | import edu.ucsb.cs156.courses.entities.GradeHistory; | |
| 6 | import edu.ucsb.cs156.courses.services.jobs.JobContext; | |
| 7 | import edu.ucsb.cs156.courses.utilities.CourseUtilities; | |
| 8 | import java.io.BufferedReader; | |
| 9 | import java.io.InputStreamReader; | |
| 10 | import java.sql.PreparedStatement; | |
| 11 | import java.sql.SQLException; | |
| 12 | import java.util.ArrayList; | |
| 13 | import java.util.HashMap; | |
| 14 | import java.util.List; | |
| 15 | import java.util.Map; | |
| 16 | import lombok.extern.slf4j.Slf4j; | |
| 17 | import org.springframework.beans.factory.annotation.Autowired; | |
| 18 | import org.springframework.http.HttpMethod; | |
| 19 | import org.springframework.jdbc.core.JdbcTemplate; | |
| 20 | import org.springframework.stereotype.Service; | |
| 21 | import org.springframework.web.client.RestTemplate; | |
| 22 | ||
| 23 | @Slf4j | |
| 24 | @Service | |
| 25 | public class GradeHistoryImportServiceImpl implements GradeHistoryImportService { | |
| 26 | ||
| 27 | public static class NullHeaderException extends RuntimeException { | |
| 28 | public NullHeaderException(String message) { | |
| 29 | super(message); | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | @Autowired private JdbcTemplate jdbcTemplate; | |
| 34 | ||
| 35 | @Autowired private RestTemplate restTemplate; | |
| 36 | ||
| 37 | @Override | |
| 38 | public void importGradesFromUrl(String url, JobContext ctx, int batchSize) throws Exception { | |
| 39 | final int[] recordsProcessed = {0}; | |
| 40 | ||
| 41 | restTemplate.execute( | |
| 42 | url, | |
| 43 | HttpMethod.GET, | |
| 44 | null, | |
| 45 | response -> { | |
| 46 | try (BufferedReader reader = | |
| 47 | new BufferedReader(new InputStreamReader(response.getBody())); | |
| 48 | CSVReader csvReader = new CSVReaderBuilder(reader).build()) { | |
| 49 | ||
| 50 | String[] header = csvReader.readNext(); | |
| 51 |
1
1. lambda$importGradesFromUrl$0 : negated conditional → KILLED |
if (header == null) throw new NullHeaderException("CSV header is missing"); |
| 52 | ||
| 53 | Map<String, Integer> col = mapHeaders(header); | |
| 54 | List<GradeHistory> buffer = new ArrayList<>(); | |
| 55 | String[] nextLine; | |
| 56 | ||
| 57 | while ((nextLine = csvReader.readNext()) != null) { | |
| 58 | List<GradeHistory> gradesFromLine = mapLineToGrades(nextLine, col); | |
| 59 | buffer.addAll(gradesFromLine); | |
| 60 | ||
| 61 |
2
1. lambda$importGradesFromUrl$0 : negated conditional → KILLED 2. lambda$importGradesFromUrl$0 : changed conditional boundary → KILLED |
if (buffer.size() >= batchSize) { |
| 62 |
1
1. lambda$importGradesFromUrl$0 : removed call to edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::flushBuffer → KILLED |
flushBuffer(buffer, batchSize); |
| 63 |
1
1. lambda$importGradesFromUrl$0 : Replaced integer addition with subtraction → KILLED |
recordsProcessed[0] += buffer.size(); |
| 64 |
1
1. lambda$importGradesFromUrl$0 : removed call to edu/ucsb/cs156/courses/services/jobs/JobContext::log → KILLED |
ctx.log("Processed " + recordsProcessed[0] + " grade history records so far."); |
| 65 |
1
1. lambda$importGradesFromUrl$0 : removed call to java/util/List::clear → KILLED |
buffer.clear(); |
| 66 | } | |
| 67 | } | |
| 68 |
1
1. lambda$importGradesFromUrl$0 : Replaced integer addition with subtraction → KILLED |
recordsProcessed[0] += buffer.size(); |
| 69 |
1
1. lambda$importGradesFromUrl$0 : removed call to edu/ucsb/cs156/courses/services/jobs/JobContext::log → KILLED |
ctx.log("Processed " + recordsProcessed[0] + " grade history records. Done!"); |
| 70 |
1
1. lambda$importGradesFromUrl$0 : removed call to edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::flushBuffer → KILLED |
flushBuffer(buffer, batchSize); |
| 71 | ||
| 72 | } catch (NullHeaderException nhe) { | |
| 73 | log.error("Error processing CSV from URL: {}", url, nhe); | |
| 74 | throw nhe; | |
| 75 | } catch (Exception e) { | |
| 76 | log.error("Error processing CSV from URL: {}", url, e); | |
| 77 | throw new RuntimeException("CSV processing failed", e); | |
| 78 | } | |
| 79 | return null; | |
| 80 | }); | |
| 81 | } | |
| 82 | ||
| 83 | private Map<String, Integer> mapHeaders(String[] header) { | |
| 84 | Map<String, Integer> map = new HashMap<>(); | |
| 85 |
2
1. mapHeaders : changed conditional boundary → KILLED 2. mapHeaders : negated conditional → KILLED |
for (int i = 0; i < header.length; i++) { |
| 86 | map.put(header[i].trim(), i); | |
| 87 | } | |
| 88 |
1
1. mapHeaders : replaced return value with Collections.emptyMap for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::mapHeaders → KILLED |
return map; |
| 89 | } | |
| 90 | ||
| 91 | private List<GradeHistory> mapLineToGrades(String[] line, Map<String, Integer> col) { | |
| 92 | List<GradeHistory> list = new ArrayList<>(); | |
| 93 | ||
| 94 | String year = line[col.get("year")]; | |
| 95 | String quarter = line[col.get("quarter")]; | |
| 96 | String yyyyq = year + CourseUtilities.quarterToDigit(quarter); | |
| 97 | String course = line[col.get("course")]; | |
| 98 | String instructor = line[col.get("instructor")]; | |
| 99 | ||
| 100 | // Map column names to cleaned Grade strings | |
| 101 | String[] gradeCols = { | |
| 102 | "Ap", "A", "Am", "Bp", "B", "Bm", "Cp", "C", "Cm", "Dp", "D", "Dm", "F", "P", "S" | |
| 103 | }; | |
| 104 | ||
| 105 | for (String grade : gradeCols) { | |
| 106 |
1
1. mapLineToGrades : negated conditional → KILLED |
if (col.containsKey(grade)) { |
| 107 | String val = line[col.get(grade)]; | |
| 108 | String convertedGrade = grade.replace("p", "+").replace("m", "-"); | |
| 109 |
1
1. mapLineToGrades : negated conditional → KILLED |
int count = (val.isEmpty()) ? 0 : Integer.parseInt(val); |
| 110 |
2
1. mapLineToGrades : negated conditional → KILLED 2. mapLineToGrades : changed conditional boundary → KILLED |
if (count > 0) { |
| 111 | list.add( | |
| 112 | GradeHistory.builder() | |
| 113 | .yyyyq(yyyyq) | |
| 114 | .course(course) | |
| 115 | .instructor(instructor) | |
| 116 | .grade(convertedGrade) | |
| 117 | .count(count) | |
| 118 | .build()); | |
| 119 | } | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | int countNP = calculateNP(line, col); | |
| 124 |
2
1. mapLineToGrades : changed conditional boundary → KILLED 2. mapLineToGrades : negated conditional → KILLED |
if (countNP > 0) { |
| 125 | list.add( | |
| 126 | GradeHistory.builder() | |
| 127 | .yyyyq(yyyyq) | |
| 128 | .course(course) | |
| 129 | .instructor(instructor) | |
| 130 | .grade("NP") | |
| 131 | .count(countNP) | |
| 132 | .build()); | |
| 133 | } | |
| 134 |
1
1. mapLineToGrades : replaced return value with Collections.emptyList for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::mapLineToGrades → KILLED |
return list; |
| 135 | } | |
| 136 | ||
| 137 | private int calculateNP(String[] line, Map<String, Integer> col) { | |
| 138 | String pVal = line[col.get("P")]; | |
| 139 | String nPnpVal = line[col.get("nPNPStudents")]; | |
| 140 | ||
| 141 |
1
1. calculateNP : negated conditional → KILLED |
int pCount = (pVal.isEmpty()) ? 0 : Integer.parseInt(pVal); |
| 142 |
1
1. calculateNP : negated conditional → KILLED |
int nPnpCount = (nPnpVal.isEmpty()) ? 0 : Integer.parseInt(nPnpVal); |
| 143 | ||
| 144 |
2
1. calculateNP : Replaced integer subtraction with addition → KILLED 2. calculateNP : replaced int return with 0 for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::calculateNP → KILLED |
return nPnpCount - pCount; |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * This method flushes the buffer to the database in batches. It is public so that it can be spied | |
| 149 | * on using Mockito in the unit tests. | |
| 150 | * | |
| 151 | * @param buffer | |
| 152 | * @param batchSize | |
| 153 | */ | |
| 154 | public void flushBuffer(List<GradeHistory> buffer, int batchSize) { | |
| 155 | // Note: 'count' is excluded from the ON clause because it is the value we want | |
| 156 | // to update | |
| 157 | String sql = | |
| 158 | """ | |
| 159 | MERGE INTO "historygrade" AS t | |
| 160 | USING (VALUES (?, ?, ?, ?, ?)) AS s(yyyyq, course, instructor, grade, count) | |
| 161 | ON (t."yyyyq" = s.yyyyq AND t."course" = s.course AND t."instructor" = s.instructor AND t."grade" = s.grade) | |
| 162 | WHEN MATCHED THEN | |
| 163 | UPDATE SET "count" = s.count | |
| 164 | WHEN NOT MATCHED THEN | |
| 165 | INSERT ("yyyyq", "course", "instructor", "grade", "count") | |
| 166 | VALUES (s.yyyyq, s.course, s.instructor, s.grade, s.count); | |
| 167 | """; | |
| 168 | ||
| 169 | jdbcTemplate.batchUpdate(sql, buffer, batchSize, this::updateEntity); | |
| 170 | } | |
| 171 | ||
| 172 | public void updateEntity(PreparedStatement ps, GradeHistory entity) throws SQLException { | |
| 173 |
1
1. updateEntity : removed call to java/sql/PreparedStatement::setString → KILLED |
ps.setString(1, entity.getYyyyq()); |
| 174 |
1
1. updateEntity : removed call to java/sql/PreparedStatement::setString → KILLED |
ps.setString(2, entity.getCourse()); |
| 175 |
1
1. updateEntity : removed call to java/sql/PreparedStatement::setString → KILLED |
ps.setString(3, entity.getInstructor()); |
| 176 |
1
1. updateEntity : removed call to java/sql/PreparedStatement::setString → KILLED |
ps.setString(4, entity.getGrade()); |
| 177 |
1
1. updateEntity : removed call to java/sql/PreparedStatement::setInt → KILLED |
ps.setInt(5, entity.getCount()); |
| 178 | } | |
| 179 | } | |
Mutations | ||
| 51 |
1.1 |
|
| 61 |
1.1 2.2 |
|
| 62 |
1.1 |
|
| 63 |
1.1 |
|
| 64 |
1.1 |
|
| 65 |
1.1 |
|
| 68 |
1.1 |
|
| 69 |
1.1 |
|
| 70 |
1.1 |
|
| 85 |
1.1 2.2 |
|
| 88 |
1.1 |
|
| 106 |
1.1 |
|
| 109 |
1.1 |
|
| 110 |
1.1 2.2 |
|
| 124 |
1.1 2.2 |
|
| 134 |
1.1 |
|
| 141 |
1.1 |
|
| 142 |
1.1 |
|
| 144 |
1.1 2.2 |
|
| 173 |
1.1 |
|
| 174 |
1.1 |
|
| 175 |
1.1 |
|
| 176 |
1.1 |
|
| 177 |
1.1 |