GradeHistoryImportServiceImpl.java

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
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_whenExpectedGradeIsNotInDataItIsHandledGracefully()]
negated conditional → KILLED

61

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
negated conditional → KILLED

2.2
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
changed conditional boundary → KILLED

62

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
removed call to edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::flushBuffer → KILLED

63

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
Replaced integer addition with subtraction → KILLED

64

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
removed call to edu/ucsb/cs156/courses/services/jobs/JobContext::log → KILLED

65

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
removed call to java/util/List::clear → KILLED

68

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
Replaced integer addition with subtraction → KILLED

69

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
removed call to edu/ucsb/cs156/courses/services/jobs/JobContext::log → KILLED

70

1.1
Location : lambda$importGradesFromUrl$0
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
removed call to edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::flushBuffer → KILLED

85

1.1
Location : mapHeaders
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_whenExpectedGradeIsNotInDataItIsHandledGracefully()]
changed conditional boundary → KILLED

2.2
Location : mapHeaders
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_whenExpectedGradeIsNotInDataItIsHandledGracefully()]
negated conditional → KILLED

88

1.1
Location : mapHeaders
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_whenExpectedGradeIsNotInDataItIsHandledGracefully()]
replaced return value with Collections.emptyMap for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::mapHeaders → KILLED

106

1.1
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_whenExpectedGradeIsNotInDataItIsHandledGracefully()]
negated conditional → KILLED

109

1.1
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
negated conditional → KILLED

110

1.1
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
negated conditional → KILLED

2.2
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
changed conditional boundary → KILLED

124

1.1
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
changed conditional boundary → KILLED

2.2
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
negated conditional → KILLED

134

1.1
Location : mapLineToGrades
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::mapLineToGrades → KILLED

141

1.1
Location : calculateNP
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testNPCount()]
negated conditional → KILLED

142

1.1
Location : calculateNP
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
negated conditional → KILLED

144

1.1
Location : calculateNP
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testNPCount()]
Replaced integer subtraction with addition → KILLED

2.2
Location : calculateNP
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_importGradesFromUrl_testBoundaryConditions()]
replaced int return with 0 for edu/ucsb/cs156/courses/services/GradeHistoryImportServiceImpl::calculateNP → KILLED

173

1.1
Location : updateEntity
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_updateEntity()]
removed call to java/sql/PreparedStatement::setString → KILLED

174

1.1
Location : updateEntity
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_updateEntity()]
removed call to java/sql/PreparedStatement::setString → KILLED

175

1.1
Location : updateEntity
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_updateEntity()]
removed call to java/sql/PreparedStatement::setString → KILLED

176

1.1
Location : updateEntity
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_updateEntity()]
removed call to java/sql/PreparedStatement::setString → KILLED

177

1.1
Location : updateEntity
Killed by : edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.services.GradeHistoryImportServiceImplTests]/[method:test_updateEntity()]
removed call to java/sql/PreparedStatement::setInt → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0