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}