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