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}