ScaffoldStartup.java

1
package edu.ucsb.cs.scaffold.startup;
2
3
import edu.ucsb.cs.scaffold.entity.Admin;
4
import edu.ucsb.cs.scaffold.entity.Course;
5
import edu.ucsb.cs.scaffold.entity.PlColor;
6
import edu.ucsb.cs.scaffold.enums.School;
7
import edu.ucsb.cs.scaffold.repository.AdminRepository;
8
import edu.ucsb.cs.scaffold.repository.CourseRepository;
9
import edu.ucsb.cs.scaffold.repository.PlColorRepository;
10
import java.io.IOException;
11
import java.nio.charset.StandardCharsets;
12
import java.util.ArrayList;
13
import java.util.Arrays;
14
import java.util.LinkedHashMap;
15
import java.util.List;
16
import java.util.Map;
17
import lombok.extern.slf4j.Slf4j;
18
import org.springframework.beans.factory.annotation.Autowired;
19
import org.springframework.beans.factory.annotation.Value;
20
import org.springframework.core.io.ClassPathResource;
21
import org.springframework.dao.DataAccessException;
22
import org.springframework.jdbc.core.JdbcTemplate;
23
import org.springframework.stereotype.Component;
24
import org.springframework.util.StreamUtils;
25
26
@Slf4j
27
@Component
28
public class ScaffoldStartup {
29
30
  static final Long SEED_COURSE_ID = 1L;
31
  static final String SEED_DATA_RESOURCE = "db/seed/concepts.sql";
32
33
  // Default PrairieLearn badge colors (issue #96), taken from the $custom-colors map in
34
  // https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/public/stylesheets/colors.scss.
35
  // ReadPLColorsJob keeps these up to date with any changes on the PrairieLearn side; this seed
36
  // only fills in rows that don't already exist so a restart never clobbers job updates.
37
  static final Map<String, String> SEED_PL_COLORS = new LinkedHashMap<>();
38
39
  static {
40
    SEED_PL_COLORS.put("red1", "#ffccbc");
41
    SEED_PL_COLORS.put("red2", "#ff6c5c");
42
    SEED_PL_COLORS.put("red3", "#c72c1c");
43
    SEED_PL_COLORS.put("pink1", "#ffbcd8");
44
    SEED_PL_COLORS.put("pink2", "#fa5c98");
45
    SEED_PL_COLORS.put("pink3", "#ba1c58");
46
    SEED_PL_COLORS.put("purple1", "#dcc6e0");
47
    SEED_PL_COLORS.put("purple2", "#9b59b6");
48
    SEED_PL_COLORS.put("purple3", "#5e147d");
49
    SEED_PL_COLORS.put("blue1", "#39d5ff");
50
    SEED_PL_COLORS.put("blue2", "#1297e0");
51
    SEED_PL_COLORS.put("blue3", "#0057a0");
52
    SEED_PL_COLORS.put("turquoise1", "#5efaf7");
53
    SEED_PL_COLORS.put("turquoise2", "#27cbc0");
54
    SEED_PL_COLORS.put("turquoise3", "#008b80");
55
    SEED_PL_COLORS.put("green1", "#8effc1");
56
    SEED_PL_COLORS.put("green2", "#2ecc71");
57
    SEED_PL_COLORS.put("green3", "#008c31");
58
    SEED_PL_COLORS.put("yellow1", "#fdeea5");
59
    SEED_PL_COLORS.put("yellow2", "#f5ce32");
60
    SEED_PL_COLORS.put("yellow3", "#d6a100");
61
    SEED_PL_COLORS.put("orange1", "#ffdcb5");
62
    SEED_PL_COLORS.put("orange2", "#ff926b");
63
    SEED_PL_COLORS.put("orange3", "#c3522b");
64
    SEED_PL_COLORS.put("brown1", "#e6bfa8");
65
    SEED_PL_COLORS.put("brown2", "#c0957c");
66
    SEED_PL_COLORS.put("brown3", "#7d5640");
67
    SEED_PL_COLORS.put("gray1", "#e0e0e0");
68
    SEED_PL_COLORS.put("gray2", "#909090");
69
    SEED_PL_COLORS.put("gray3", "#505050");
70
  }
71
72
  @Value("#{'${app.admin.emails}'.split(',')}")
73
  List<String> adminEmails;
74
75
  @Autowired AdminRepository adminRepository;
76
77
  @Autowired CourseRepository courseRepository;
78
79
  @Autowired PlColorRepository plColorRepository;
80
81
  @Autowired JdbcTemplate jdbcTemplate;
82
83
  public void alwaysRunOnStartup() {
84
    log.info("ScaffoldStartup.alwaysRunOnStartup called");
85
86
    try {
87 1 1. alwaysRunOnStartup : removed call to java/util/List::forEach → KILLED
      adminEmails.forEach(
88
          (email) -> {
89
            Admin admin = new Admin(email.strip());
90
            adminRepository.save(admin);
91
          });
92
    } catch (Exception e) {
93
      log.error("Error loading ADMIN_EMAILS into admins table:", e);
94
    }
95
96
    // Temporary demo/test seed data for the concept graph feature. This creates course 1
97
    // (if it doesn't already exist) and loads db/seed/concepts.sql, ignoring errors for
98
    // records that already exist. Remove this once the app is past the demo/testing stage.
99
    try {
100 1 1. alwaysRunOnStartup : removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedCourseOne → KILLED
      seedCourseOne();
101 1 1. alwaysRunOnStartup : removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedConceptsSqlFile → KILLED
      seedConceptsSqlFile();
102
    } catch (Exception e) {
103
      log.error("Error seeding concept graph demo data:", e);
104
    }
105
106
    try {
107 1 1. alwaysRunOnStartup : removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedPlColors → KILLED
      seedPlColors();
108
    } catch (Exception e) {
109
      log.error("Error seeding pl_color table:", e);
110
    }
111
  }
112
113
  private void seedPlColors() {
114 1 1. seedPlColors : removed call to java/util/Map::forEach → KILLED
    SEED_PL_COLORS.forEach(
115
        (colorName, hexCode) -> {
116 1 1. lambda$seedPlColors$1 : negated conditional → KILLED
          if (!plColorRepository.existsById(colorName)) {
117
            plColorRepository.save(PlColor.builder().colorName(colorName).hexCode(hexCode).build());
118
          }
119
        });
120
  }
121
122
  private void seedCourseOne() {
123 1 1. seedCourseOne : negated conditional → KILLED
    if (courseRepository.existsById(SEED_COURSE_ID)) {
124
      return;
125
    }
126
    Course course =
127
        Course.builder()
128
            .courseName("CMPSC 8")
129
            .instructorEmail("phtcon@ucsb.edu")
130
            .term("S26")
131
            .school(School.UCSB)
132
            .build();
133
    courseRepository.save(course);
134
    log.info("Created seed course: CMPSC 8");
135
  }
136
137
  private void seedConceptsSqlFile() throws IOException {
138
    // Subconcepts have a NULL name, so re-running the seed script would not trip the
139
    // (course_id, name) unique constraint and would insert duplicates; skip the whole
140
    // script once any concepts exist for the seed course.
141 1 1. seedConceptsSqlFile : negated conditional → KILLED
    if (seedConceptsAlreadyPresent()) {
142
      return;
143
    }
144
    String sql =
145
        StreamUtils.copyToString(
146
            new ClassPathResource(SEED_DATA_RESOURCE).getInputStream(), StandardCharsets.UTF_8);
147 1 1. seedConceptsSqlFile : removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::executeSeedStatements → KILLED
    executeSeedStatements(sql);
148
  }
149
150
  boolean seedConceptsAlreadyPresent() {
151
    Integer count =
152
        jdbcTemplate.queryForObject(
153
            "SELECT COUNT(*) FROM concepts WHERE course_id = ?", Integer.class, SEED_COURSE_ID);
154 4 1. seedConceptsAlreadyPresent : negated conditional → KILLED
2. seedConceptsAlreadyPresent : changed conditional boundary → KILLED
3. seedConceptsAlreadyPresent : negated conditional → KILLED
4. seedConceptsAlreadyPresent : replaced boolean return with true for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedConceptsAlreadyPresent → KILLED
    return count != null && count > 0;
155
  }
156
157
  void executeSeedStatements(String sql) {
158
    for (String rawStatement : splitSqlStatements(sql)) {
159
      String cleaned = stripLeadingCommentsAndBlankLines(rawStatement);
160 1 1. executeSeedStatements : negated conditional → KILLED
      if (cleaned.isEmpty()) {
161
        continue;
162
      }
163
      try {
164 1 1. executeSeedStatements : removed call to org/springframework/jdbc/core/JdbcTemplate::execute → KILLED
        jdbcTemplate.execute(cleaned);
165
      } catch (DataAccessException e) {
166
        log.debug("Skipping seed statement (likely already applied): {}", e.getMessage());
167
      }
168
    }
169
  }
170
171
  /**
172
   * Removes leading blank and "--" comment lines from a single SQL statement, since those aren't
173
   * separated from the following real statement by their own semicolon and would otherwise end up
174
   * glued to the front of it. Only LEADING lines are removed (stopping at the first line of real
175
   * content) so that a blank line inside a multi-paragraph string value later in the same statement
176
   * is left untouched.
177
   */
178
  static String stripLeadingCommentsAndBlankLines(String statement) {
179
    String[] lines = statement.split("\n", -1);
180
    int start = 0;
181 2 1. stripLeadingCommentsAndBlankLines : negated conditional → KILLED
2. stripLeadingCommentsAndBlankLines : changed conditional boundary → KILLED
    while (start < lines.length) {
182
      String trimmedLine = lines[start].strip();
183 2 1. stripLeadingCommentsAndBlankLines : negated conditional → KILLED
2. stripLeadingCommentsAndBlankLines : negated conditional → KILLED
      if (trimmedLine.isEmpty() || trimmedLine.startsWith("--")) {
184 1 1. stripLeadingCommentsAndBlankLines : Changed increment from 1 to -1 → KILLED
        start++;
185
      } else {
186
        break;
187
      }
188
    }
189
    String result = String.join("\n", Arrays.copyOfRange(lines, start, lines.length));
190 1 1. stripLeadingCommentsAndBlankLines : replaced return value with "" for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::stripLeadingCommentsAndBlankLines → KILLED
    return result.strip();
191
  }
192
193
  /**
194
   * Splits a SQL script into individual statements on top-level semicolons, ignoring semicolons
195
   * that appear inside single-quoted string literals (including '' escaped quotes, since a pair of
196
   * quote-toggles cancels out and leaves the parser in the correct state).
197
   */
198
  static List<String> splitSqlStatements(String sql) {
199
    List<String> statements = new ArrayList<>();
200
    StringBuilder current = new StringBuilder();
201
    boolean inSingleQuotedString = false;
202 2 1. splitSqlStatements : changed conditional boundary → KILLED
2. splitSqlStatements : negated conditional → KILLED
    for (int i = 0; i < sql.length(); i++) {
203
      char c = sql.charAt(i);
204
      current.append(c);
205 1 1. splitSqlStatements : negated conditional → KILLED
      if (c == '\'') {
206 1 1. splitSqlStatements : negated conditional → KILLED
        inSingleQuotedString = !inSingleQuotedString;
207 2 1. splitSqlStatements : negated conditional → KILLED
2. splitSqlStatements : negated conditional → KILLED
      } else if (c == ';' && !inSingleQuotedString) {
208
        statements.add(current.toString());
209 1 1. splitSqlStatements : removed call to java/lang/StringBuilder::setLength → KILLED
        current.setLength(0);
210
      }
211
    }
212 1 1. splitSqlStatements : negated conditional → KILLED
    if (!current.toString().isBlank()) {
213
      statements.add(current.toString());
214
    }
215 1 1. splitSqlStatements : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::splitSqlStatements → KILLED
    return statements;
216
  }
217
}

Mutations

87

1.1
Location : alwaysRunOnStartup
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_handles_course_seeding_exception_gracefully()]
removed call to java/util/List::forEach → KILLED

100

1.1
Location : alwaysRunOnStartup
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_creates_seed_course_when_missing()]
removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedCourseOne → KILLED

101

1.1
Location : alwaysRunOnStartup
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_loads_the_bundled_seed_data_file_without_error()]
removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedConceptsSqlFile → KILLED

107

1.1
Location : alwaysRunOnStartup
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_seeds_all_default_pl_colors_when_table_is_empty()]
removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedPlColors → KILLED

114

1.1
Location : seedPlColors
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_seeds_all_default_pl_colors_when_table_is_empty()]
removed call to java/util/Map::forEach → KILLED

116

1.1
Location : lambda$seedPlColors$1
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_skips_pl_colors_that_already_exist()]
negated conditional → KILLED

123

1.1
Location : seedCourseOne
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_skips_seed_course_creation_when_already_present()]
negated conditional → KILLED

141

1.1
Location : seedConceptsSqlFile
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_skips_the_seed_data_file_when_concepts_already_exist()]
negated conditional → KILLED

147

1.1
Location : seedConceptsSqlFile
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:alwaysRunOnStartup_loads_the_bundled_seed_data_file_without_error()]
removed call to edu/ucsb/cs/scaffold/startup/ScaffoldStartup::executeSeedStatements → KILLED

154

1.1
Location : seedConceptsAlreadyPresent
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:seedConceptsAlreadyPresent_is_false_when_count_is_null()]
negated conditional → KILLED

2.2
Location : seedConceptsAlreadyPresent
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:seedConceptsAlreadyPresent_is_false_when_count_is_zero()]
changed conditional boundary → KILLED

3.3
Location : seedConceptsAlreadyPresent
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:seedConceptsAlreadyPresent_is_true_when_concepts_exist()]
negated conditional → KILLED

4.4
Location : seedConceptsAlreadyPresent
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:seedConceptsAlreadyPresent_is_false_when_count_is_null()]
replaced boolean return with true for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::seedConceptsAlreadyPresent → KILLED

160

1.1
Location : executeSeedStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:executeSeedStatements_skips_a_statement_that_is_entirely_a_comment()]
negated conditional → KILLED

164

1.1
Location : executeSeedStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:executeSeedStatements_skips_a_statement_that_is_entirely_a_comment()]
removed call to org/springframework/jdbc/core/JdbcTemplate::execute → KILLED

181

1.1
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:stripLeadingCommentsAndBlankLines_returns_empty_when_the_whole_statement_is_a_comment()]
negated conditional → KILLED

2.2
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:stripLeadingCommentsAndBlankLines_returns_empty_when_the_whole_statement_is_a_comment()]
changed conditional boundary → KILLED

183

1.1
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:stripLeadingCommentsAndBlankLines_returns_empty_when_the_whole_statement_is_a_comment()]
negated conditional → KILLED

2.2
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:executeSeedStatements_skips_blank_and_comment_only_lines()]
negated conditional → KILLED

184

1.1
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:stripLeadingCommentsAndBlankLines_returns_empty_when_the_whole_statement_is_a_comment()]
Changed increment from 1 to -1 → KILLED

190

1.1
Location : stripLeadingCommentsAndBlankLines
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:executeSeedStatements_skips_a_statement_that_is_entirely_a_comment()]
replaced return value with "" for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::stripLeadingCommentsAndBlankLines → KILLED

202

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
changed conditional boundary → KILLED

2.2
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

205

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

206

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

207

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

2.2
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

209

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
removed call to java/lang/StringBuilder::setLength → KILLED

212

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
negated conditional → KILLED

215

1.1
Location : splitSqlStatements
Killed by : edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.startup.ScaffoldStartupTests]/[method:splitSqlStatements_ignores_semicolons_inside_quoted_strings()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/startup/ScaffoldStartup::splitSqlStatements → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0