001package edu.pdx.cs.joy.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import edu.pdx.cs.joy.grader.gradebook.Student; 005import edu.pdx.cs.joy.grader.gradebook.XmlDumper; 006import edu.pdx.cs.joy.grader.gradebook.XmlHelper; 007import jakarta.activation.DataHandler; 008import jakarta.activation.DataSource; 009import jakarta.mail.*; 010import jakarta.mail.internet.*; 011import jakarta.mail.util.ByteArrayDataSource; 012import org.w3c.dom.Document; 013 014import javax.xml.transform.TransformerException; 015import java.io.*; 016import java.util.function.Consumer; 017 018/** 019 * This program presents a survey that all students in The Joy of Coding should 020 * answer. It emails the results of the survey to the TA and emails a 021 * receipt back to the student. 022 */ 023public class Survey extends EmailSender { 024 @VisibleForTesting 025 static final String STUDENT_XML_FILE_NAME = "me.xml"; 026 027 private final PrintWriter out; 028 private final PrintWriter err; 029 private final BufferedReader in; 030 031 private boolean sendEmail = true; 032 private final File xmlFileDir; 033 private File gitCheckoutDir; 034 035 public Survey(PrintStream out, PrintStream err, InputStream in, File xmlFileDir, File gitCheckoutDir) { 036 this.out = new PrintWriter(out, true); 037 this.err = new PrintWriter(err, true); 038 this.in = new BufferedReader(new InputStreamReader(in)); 039 this.xmlFileDir = xmlFileDir; 040 this.gitCheckoutDir = gitCheckoutDir; 041 } 042 043 public static boolean hasPdxDotEduEmail(Student student) { 044 String email = student.getEmail(); 045 return email != null && email.endsWith("@pdx.edu"); 046 } 047 048 /** 049 * Returns a textual summary of a <code>Student</code> 050 */ 051 private String getSummary(Student student) { 052 StringBuilder sb = new StringBuilder(); 053 sb.append("Name: ").append(student.getFullName()).append("\n"); 054 sb.append("UNIX login: ").append(student.getId()).append("\n"); 055 if (student.getEmail() != null) { 056 sb.append("Email: ").append(student.getEmail()).append("\n"); 057 } 058 if (student.getMajor() != null) { 059 sb.append("Major: ").append(student.getMajor()).append("\n"); 060 } 061 sb.append("Enrolled in: ").append(student.getEnrolledSection().asString()).append("\n"); 062 if (student.getGitHubUserName() != null) { 063 sb.append("GitHub User Name: ").append(student.getGitHubUserName()).append("\n"); 064 } 065 return sb.toString(); 066 } 067 068 /** 069 * Ask the student a question and return his response 070 */ 071 private String ask(String question) { 072 out.print(breakUpInto80CharacterLines(question)); 073 out.print(" "); 074 out.flush(); 075 076 String response = null; 077 try { 078 response = in.readLine(); 079 080 } catch (IOException ex) { 081 printErrorMessageAndExit("** IOException while reading response", ex); 082 } 083 084 return response; 085 } 086 087 /** 088 * Prints out usage information for this program 089 */ 090 private void usage() { 091 err.println("\nusage: java Survey [options]"); 092 err.println(" where [options] are:"); 093 err.println(" -mailServer serverName Mail server to send mail"); 094 err.println(" -saveStudentXmlFile Save a copy of the generated student.xml file"); 095 err.println("\n"); 096 System.exit(1); 097 } 098 099 public static void main(String[] args) { 100 File currentDirectory = new File(System.getProperty("user.dir")); 101 Survey survey = new Survey(System.out, System.err, System.in, currentDirectory, currentDirectory); 102 survey.takeSurvey(args); 103 } 104 105 @VisibleForTesting 106 void takeSurvey(String... args) { 107 parseCommandLine(args); 108 109 exitIfStudentXmlFileAlreadyExists(); 110 111 printIntroduction(); 112 113 Student student = gatherStudentInformation(); 114 115 String learn = ask("What do you hope to learn in The Joy of Coding?"); 116 String comments = ask("What else would you like to tell me?"); 117 118 addNotesToStudent(student, learn, comments); 119 120 writeStudentXmlToFile(getXmlBytes(student)); 121 122 emailSurveyResults(student, learn, comments); 123 124 } 125 126 private void exitIfStudentXmlFileAlreadyExists() { 127 File studentXmlFile = new File(this.xmlFileDir, STUDENT_XML_FILE_NAME); 128 if (studentXmlFile.exists()) { 129 String message = "\nIt looks like you've already run the Survey program.\n" + 130 "\nThe student XML file \"" + STUDENT_XML_FILE_NAME + 131 "\" already exists in the directory \"" + this.xmlFileDir + "\".\n" + 132 "\nYou don't need to run the Survey program again.\n" + 133 "\nIf you want to run it again, please delete the file \"" + 134 STUDENT_XML_FILE_NAME + "\" and try again."; 135 printErrorMessageAndExit(message); 136 } 137 } 138 139 private void addNotesToStudent(Student student, String learn, String comments) { 140 if (isNotEmpty(learn)) { 141 student.addNote(student.getFullName() + " would like to learn " + learn); 142 } 143 144 if (isNotEmpty(comments)) { 145 student.addNote(student.getFullName() + " has these comments: " + comments); 146 } 147 } 148 149 private Student gatherStudentInformation() { 150 String firstName = ask("What is your first name?"); 151 String lastName = ask("What is your last name?"); 152 String nickName = ask("What is your nickname? (Leave blank if " + 153 "you don't have one)"); 154 String id = ask("MANDATORY: What is your MCECS UNIX login id?"); 155 156 if (isEmpty(id)) { 157 printErrorMessageAndExit("** You must enter a valid MCECS UNIX login id"); 158 159 } else if (isEmailAddress(id)) { 160 printErrorMessageAndExit("** Your student id cannot be an email address"); 161 162 } else if (!isJavaIdentifier(id)) { 163 printErrorMessageAndExit("** Your student id must be a valid Java identifier"); 164 } 165 166 Student student = new Student(id); 167 setValueIfNotEmpty(firstName, student::setFirstName); 168 setValueIfNotEmpty(lastName, student::setLastName); 169 setValueIfNotEmpty(nickName, student::setNickName); 170 171 askQuestionAndSetValue("What is your email address (must be @pdx.edu)?", student::setEmail); 172 if (!hasPdxDotEduEmail(student)) { 173 printErrorMessageAndExit("** Your email address must be @pdx.edu"); 174 } 175 176 askQuestionAndSetValue("What is your major?", student::setMajor); 177 178 askEnrolledSectionQuestion(student); 179 askGitHubUserNameQuestion(student); 180 181 return student; 182 } 183 184 private void askGitHubUserNameQuestion(Student student) { 185 String gitHubUserName = findGitHubUserName(); 186 String okay = ask("Is it okay to record your GitHub username of \"" + gitHubUserName 187 + "\" so you can be added to the shared GitHub repo for Pair/Mob Programming? [y/n]"); 188 if (okay.equalsIgnoreCase("y")) { 189 student.setGitHubUserName(gitHubUserName); 190 } 191 } 192 193 private String findGitHubUserName() { 194 File dotGit = new File(this.gitCheckoutDir, ".git"); 195 File config = new File(dotGit, "config"); 196 if (config.isFile()) { 197 try { 198 GitConfigParser parser = new GitConfigParser(new FileReader(config)); 199 GitHubUserNameFinder finder = new GitHubUserNameFinder(); 200 parser.parse(finder); 201 return finder.getGitHubUserName(); 202 203 } catch (IOException e) { 204 return null; 205 } 206 } 207 return null; 208 } 209 210 @VisibleForTesting 211 static boolean isEmailAddress(String id) { 212 try { 213 new InternetAddress(id, true /* strict */); 214 return true; 215 216 } catch (AddressException e) { 217 return false; 218 } 219 } 220 221 @VisibleForTesting 222 static boolean isJavaIdentifier(String id) { 223 if (id == null || id.equals("")) { 224 return false; 225 } 226 227 if (!Character.isJavaIdentifierStart(id.charAt(0))) { 228 return false; 229 } 230 231 for (int i = 1; i < id.length(); i++) { 232 char c = id.charAt(i); 233 if (!Character.isJavaIdentifierPart(c)) { 234 return false; 235 } 236 } 237 238 return true; 239 } 240 241 private void askEnrolledSectionQuestion(Student student) { 242 String answer = ask("MANDATORY: Are you enrolled in the undergraduate or graduate section of this course? [u/g]"); 243 if (isEmpty(answer)) { 244 printErrorMessageAndExit("Missing enrolled section. Please enter a \"u\" or \"g\""); 245 } 246 247 if (answer.toLowerCase().startsWith("u")) { 248 student.setEnrolledSection(Student.Section.UNDERGRADUATE); 249 250 } else if (answer.toLowerCase().startsWith("g")) { 251 student.setEnrolledSection(Student.Section.GRADUATE); 252 253 } else { 254 printErrorMessageAndExit("Unknown section \"" + answer + "\". Please enter a \"u\" or \"g\""); 255 } 256 257 } 258 259 private void askQuestionAndSetValue(String question, Consumer<String> setter) { 260 String answer = ask(question); 261 setValueIfNotEmpty(answer, setter); 262 } 263 264 static void setValueIfNotEmpty(String string, Consumer<String> setter) { 265 if (isNotEmpty(string)) { 266 setter.accept(string); 267 } 268 } 269 270 private void emailSurveyResults(Student student, String learn, String comments) { 271 String summary = verifyInformation(student); 272 273 if (sendEmail) { 274 // Email the results of the survey to the TA and CC the student 275 out.println("Emailing your information to the Grader"); 276 277 MimeMessage message = createEmailMessage(student); 278 MimeBodyPart textPart = createEmailText(learn, comments, summary); 279 MimeBodyPart xmlFilePart = createXmlAttachment(student); 280 addAttachmentsAndSendEmail(message, textPart, xmlFilePart); 281 } 282 } 283 284 private void addAttachmentsAndSendEmail(MimeMessage message, MimeBodyPart textPart, MimeBodyPart filePart) { 285 // Finally, add the attachments to the message and send it 286 try { 287 Multipart mp = new MimeMultipart(); 288 mp.addBodyPart(textPart); 289 mp.addBodyPart(filePart); 290 291 message.setContent(mp); 292 293 Transport.send(message); 294 295 logSentEmail(message); 296 297 } catch (MessagingException ex) { 298 printErrorMessageAndExit("** Exception while adding parts and sending", ex); 299 } 300 } 301 302 private void logSentEmail(MimeMessage message) throws MessagingException { 303 StringBuilder sb = new StringBuilder(); 304 sb.append("\nAn email with with subject \""); 305 sb.append(message.getSubject()); 306 sb.append("\" was sent to "); 307 308 Address[] recipients = message.getAllRecipients(); 309 for (int i = 0; i < recipients.length; i++) { 310 Address recipient = recipients[i]; 311 sb.append(recipient); 312 if (i < recipients.length - 2) { 313 sb.append(", "); 314 315 } else if (i < recipients.length - 1) { 316 sb.append(" and "); 317 } 318 } 319 320 out.println(breakUpInto80CharacterLines(sb.toString())); 321 } 322 323 private MimeBodyPart createXmlAttachment(Student student) { 324 byte[] xmlBytes = getXmlBytes(student); 325 326 DataSource ds = new ByteArrayDataSource(xmlBytes, "text/xml"); 327 DataHandler dh = new DataHandler(ds); 328 MimeBodyPart filePart = new MimeBodyPart(); 329 try { 330 String xmlFileTitle = student.getId() + ".xml"; 331 332 filePart.setDataHandler(dh); 333 filePart.setFileName(xmlFileTitle); 334 filePart.setDescription("XML file for " + student.getFullName()); 335 336 } catch (MessagingException ex) { 337 printErrorMessageAndExit("** Exception with file part", ex); 338 } 339 return filePart; 340 } 341 342 private void writeStudentXmlToFile(byte[] xmlBytes) { 343 File file = new File(this.xmlFileDir, STUDENT_XML_FILE_NAME); 344 try (FileOutputStream fos = new FileOutputStream(file)) { 345 fos.write(xmlBytes); 346 fos.flush(); 347 348 } catch (IOException e) { 349 printErrorMessageAndExit("Could not write student XML file: " + file, e); 350 } 351 352 out.println("\nSaved student XML file to " + file + "\n"); 353 } 354 355 private MimeBodyPart createEmailText(String learn, String comments, String summary) { 356 // Create the text portion of the message 357 StringBuilder text = new StringBuilder(); 358 text.append("Results of The Joy of Coding Survey:\n\n"); 359 text.append(summary); 360 text.append("\n\nWhat do you hope to learn in The Joy of Coding?\n\n"); 361 text.append(learn); 362 text.append("\n\nIs there anything else you'd like to tell me?\n\n"); 363 text.append(comments); 364 text.append("\n\nThanks for filling out this survey!\n\nDave"); 365 366 MimeBodyPart textPart = new MimeBodyPart(); 367 try { 368 textPart.setContent(text.toString(), "text/plain"); 369 370 // Try not to display text as separate attachment 371 textPart.setDisposition("inline"); 372 373 } catch (MessagingException ex) { 374 printErrorMessageAndExit("** Exception with text part", ex); 375 } 376 return textPart; 377 } 378 379 private MimeMessage createEmailMessage(Student student) { 380 MimeMessage message = null; 381 try { 382 InternetAddress studentEmail = newInternetAddress(student.getEmail(), student.getFullName()); 383 String subject = "The Joy of Coding Survey for " + student.getFullName(); 384 message = newEmailTo(newEmailSession(false), TA_EMAIL).from(studentEmail).withSubject(subject).createMessage(); 385 386 InternetAddress[] cc = { studentEmail }; 387 message.setRecipients(Message.RecipientType.CC, cc); 388 389 } catch (AddressException ex) { 390 printErrorMessageAndExit("** Exception with email address", ex); 391 392 } catch (MessagingException ex) { 393 printErrorMessageAndExit("** Exception while setting recipients email", ex); 394 } 395 return message; 396 } 397 398 private byte[] getXmlBytes(Student student) { 399 // Create a temporary "file" to hold the Student's XML file. We 400 // use a byte array so that potentially sensitive data (SSN, etc.) 401 // is not written to disk 402 byte[] bytes = null; 403 404 Document xmlDoc = XmlDumper.toXml(student); 405 406 407 try { 408 bytes = XmlHelper.getBytesForXmlDocument(xmlDoc); 409 410 } catch (TransformerException ex) { 411 ex.printStackTrace(System.err); 412 System.exit(1); 413 } 414 return bytes; 415 } 416 417 private String verifyInformation(Student student) { 418 String summary = getSummary(student); 419 420 out.println("\nYou entered the following information about " + 421 "yourself:\n"); 422 out.println(summary); 423 424 String verify = ask("\nIs this information correct? [y/n]"); 425 if (!verify.equals("y")) { 426 printErrorMessageAndExit("** Not sending information. Exiting."); 427 } 428 return summary; 429 } 430 431 private void printErrorMessageAndExit(String message) { 432 printErrorMessageAndExit(message, null); 433 } 434 435 private void printErrorMessageAndExit(String message, Throwable ex) { 436 err.println(message); 437 if (ex != null) { 438 ex.printStackTrace(err); 439 } 440 System.exit(1); 441 } 442 443 private static boolean isNotEmpty(String string) { 444 return string != null && !string.equals(""); 445 } 446 447 private boolean isEmpty(String string) { 448 return string == null || string.equals(""); 449 } 450 451 private void printIntroduction() { 452 // Ask the student a bunch of questions 453 String welcome = 454 "Welcome to the Survey Program. I'd like to ask you a couple of " + 455 "questions about yourself. Except for your UNIX login id and the section " + 456 "that you are enrolled in, no question " + 457 "is mandatory. Your answers will be emailed to the Grader and a receipt " + 458 "will be emailed to you."; 459 460 out.println(""); 461 out.println(breakUpInto80CharacterLines(welcome)); 462 out.println(""); 463 } 464 465 @VisibleForTesting 466 static String breakUpInto80CharacterLines(String message) { 467 StringBuilder sb = new StringBuilder(); 468 int currentLineLength = 0; 469 String[] words = message.split(" "); 470 for (String word : words) { 471 if (currentLineLength + word.length() > 80) { 472 sb.append('\n'); 473 sb.append(word); 474 currentLineLength = word.length(); 475 476 } else { 477 if (currentLineLength > 0) { 478 sb.append(' '); 479 } 480 sb.append(word); 481 currentLineLength += word.length() + 1; 482 } 483 484 } 485 return sb.toString(); 486 } 487 488 private void parseCommandLine(String[] args) { 489 // Parse the command line 490 for (int i = 0; i < args.length; i++) { 491 String arg = args[i]; 492 if (arg.equals("-mailServer")) { 493 if (++i >= args.length) { 494 err.println("** Missing mail server name"); 495 usage(); 496 } 497 498 serverName = arg; 499 500 } else if (arg.equals("-noEmail")) { 501 sendEmail = false; 502 503 } else if (arg.startsWith("-")) { 504 err.println("** Unknown command line option: " + arg); 505 usage(); 506 507 } else { 508 err.println("** Spurious command line: " + arg); 509 usage(); 510 } 511 } 512 } 513 514}