001package edu.pdx.cs410J.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import edu.pdx.cs410J.ParserException; 005import edu.pdx.cs410J.grader.gradebook.Student; 006import edu.pdx.cs410J.grader.gradebook.XmlStudentParser; 007import jakarta.activation.DataHandler; 008import jakarta.activation.DataSource; 009import jakarta.activation.FileDataSource; 010import jakarta.mail.MessagingException; 011import jakarta.mail.Multipart; 012import jakarta.mail.Transport; 013import jakarta.mail.internet.*; 014 015import java.io.*; 016import java.time.LocalDateTime; 017import java.time.format.DateTimeFormatter; 018import java.time.format.DateTimeParseException; 019import java.time.format.FormatStyle; 020import java.util.*; 021import java.util.jar.Attributes; 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024import java.util.stream.Collectors; 025import java.util.stream.Stream; 026 027/** 028 * This class is used to submit assignments in CS410J. The user 029 * specified his or her email address as well as the base directory 030 * for his/her source files on the command line. The directory is 031 * searched recursively for files that are allowed to be submitted. 032 * Those files are 033 * placed in a zip file and emailed to the grader. A confirmation 034 * email is sent to the submitter. 035 * 036 * More information about the JavaMail API can be found at: 037 * 038 * <a href="http://java.sun.com/products/javamail"> 039 * http://java.sun.com/products/javamail</a> 040 * 041 * @author David Whitlock 042 * @since Fall 2000 (Refactored to use fewer static methods in Spring 2006) 043 */ 044public class Submit extends EmailSender { 045 046 private static final PrintWriter out = new PrintWriter(System.out, true); 047 private static final PrintWriter err = new PrintWriter(System.err, true); 048 049 ///////////////////// Instance Fields ////////////////////////// 050 051 /** 052 * The name of the project being submitted 053 */ 054 private String projName = null; 055 056 /** 057 * The name of the user (student) submits the project 058 */ 059 private String userName = null; 060 061 /** 062 * The submitter's email address 063 */ 064 private String userEmail = null; 065 066 /** 067 * The submitter's user id 068 */ 069 private String userId = null; 070 071 /** 072 * A comment describing the project 073 */ 074 private String comment = null; 075 076 /** 077 * Should the execution of this program be logged? 078 */ 079 private boolean debug = false; 080 081 /** 082 * Should the generated zip file be saved? 083 */ 084 private boolean saveZip = false; 085 086 private boolean sendEmails = true; 087 088 /** 089 * The time at which the project was submitted 090 */ 091 private LocalDateTime submitTime = null; 092 093 /** 094 * The names of the files to be submitted 095 */ 096 private final Set<String> fileNames = new HashSet<>(); 097 098 private boolean isSubmittingKoans = false; 099 private boolean sendReceipt = true; 100 private String studentXmlFileName; 101 private Double estimatedHours; 102 private final CurrentTimeProvider currentTimeProvider; 103 private BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 104 105 /////////////////////// Constructors ///////////////////////// 106 107 /** 108 * Creates a new <code>Submit</code> program 109 */ 110 Submit() { 111 this(LocalDateTime::now); 112 } 113 114 @VisibleForTesting 115 Submit(CurrentTimeProvider currentTimeProvider) { 116 this.currentTimeProvider = currentTimeProvider; 117 } 118 119 ///////////////////// Instance Methods /////////////////////// 120 121 /** 122 * Sets the name of the SMTP server that is used to send emails 123 */ 124 void setEmailServerHostName(String serverName) { 125 EmailSender.serverName = serverName; 126 } 127 128 /** 129 * Sets whether or not the progress of the submission should be logged. 130 */ 131 public void setDebug(boolean debug) { 132 this.debug = debug; 133 } 134 135 /** 136 * Sets whether or not the zip file generated by the submission 137 * should be saved. 138 */ 139 private void saveZip() { 140 this.saveZip = true; 141 } 142 143 private void doNotSendEmails() { 144 this.sendEmails = false; 145 } 146 147 /** 148 * Sets the comment for this submission 149 */ 150 private void setComment(String comment) { 151 this.comment = comment; 152 } 153 154 /** 155 * Sets the name of project being submitted 156 */ 157 void setProjectName(String projName) { 158 this.projName = projName; 159 } 160 161 /** 162 * Adds the file with the given name to the list of files to be 163 * submitted. 164 */ 165 void addFile(String fileName) { 166 this.fileNames.add(fileName); 167 } 168 169 /** 170 * Validates the state of this submission 171 * 172 * @throws IllegalStateException If any state is incorrect or missing 173 */ 174 private void validate() { 175 validateProjectName(); 176 177 if (projName == null) { 178 throw new IllegalStateException("Missing project name"); 179 } 180 181 validateStudentXmlFile(); 182 183 if (userName == null) { 184 throw new IllegalStateException("Missing student name"); 185 } 186 187 if (userId == null) { 188 throw new IllegalStateException("Missing login id"); 189 } 190 191 if (isNineDigitStudentId(userId)) { 192 throw new IllegalStateException(loginIdShouldNotBeStudentId(userId)); 193 } 194 195 if (looksLikeAnEmailAddress(userId)) { 196 throw new IllegalStateException(loginIdShouldNotBeEmailAddress(userId)); 197 } 198 199 if (userEmail == null) { 200 throw new IllegalStateException("Missing email address"); 201 202 } else { 203 // Make sure user's email is okay 204 try { 205 new InternetAddress(userEmail); 206 207 } catch (AddressException ex) { 208 String m = "Invalid email address: " + userEmail; 209 throw new IllegalStateException(m, ex); 210 } 211 } 212 } 213 214 private void validateStudentXmlFile() { 215 if (this.studentXmlFileName == null) { 216 throw new IllegalStateException("Missing Student XML File"); 217 } 218 219 File studentXmlFile = new File(this.studentXmlFileName); 220 if (!studentXmlFile.exists()) { 221 throw new IllegalStateException("Student XML file \"" + studentXmlFile + "\" doesn't exist"); 222 } 223 224 Student student; 225 try { 226 student = new XmlStudentParser(studentXmlFile).parseStudent(); 227 } catch (ParserException | IOException ex) { 228 throw new IllegalStateException("Could not parse Student XML file \"" + studentXmlFile + "\"", ex); 229 } 230 231 setStudent(student); 232 233 } 234 235 @VisibleForTesting 236 void setStudent(Student student) { 237 this.userId = student.getId(); 238 this.userName = student.getFirstName() + " " + student.getLastName(); 239 this.userEmail = student.getEmail(); 240 } 241 242 private String loginIdShouldNotBeEmailAddress(String userId) { 243 return "Your login id (" + userId + ") should not be an email address"; 244 } 245 246 @VisibleForTesting 247 boolean looksLikeAnEmailAddress(String userId) { 248 return userId.contains("@"); 249 } 250 251 @VisibleForTesting 252 boolean isNineDigitStudentId(String userId) { 253 return userId.matches("^[0-9]{9}$"); 254 } 255 256 private String loginIdShouldNotBeStudentId(String userId) { 257 return "Your login id (" + userId + ") should not be your 9-digit student id"; 258 } 259 260 @VisibleForTesting 261 void validateProjectName() { 262 List<String> validProjectNames = fetchListOfValidProjectNames(); 263 if (!validProjectNames.contains(projName)) { 264 String message = "\"" + projName + "\" is not in the list of valid project names: " + validProjectNames; 265 throw new IllegalStateException(message); 266 } 267 } 268 269 private List<String> fetchListOfValidProjectNames() { 270 return fetchListOfStringsFromResource("project-names"); 271 } 272 273 private List<String> fetchListOfStringsFromResource(String resourceName) { 274 InputStream stream = this.getClass().getResourceAsStream(resourceName); 275 if (stream == null) { 276 throw new IllegalStateException("Cannot load " + resourceName); 277 } 278 279 List<String> strings = new ArrayList<>(); 280 try { 281 282 InputStreamReader isr = new InputStreamReader(stream); 283 BufferedReader br = new BufferedReader(isr); 284 while (br.ready()) { 285 strings.add(br.readLine().trim()); 286 } 287 288 } catch (IOException ex) { 289 err.println("** WARNING: Problems while reading " + resourceName + ": " + ex.getMessage()); 290 } 291 292 return strings; 293 } 294 295 /** 296 * Submits the project to the grader 297 * 298 * @param verify Should the user be prompted to verify his submission? 299 * @return Whether or not the submission actually occurred 300 * @throws IllegalStateException If no source files were found 301 */ 302 public boolean submit(boolean verify) throws IOException, MessagingException { 303 // Recursively search the source directory for .java files 304 Set<File> sourceFiles = searchForSourceFiles(fileNames); 305 306 db(sourceFiles.size() + " source files found"); 307 308 if (sourceFiles.size() == 0) { 309 String s = "No source files were found."; 310 throw new IllegalStateException(s); 311 } else if (sourceFiles.size() < 3) { 312 String s = "Too few source files were submitted. Each project requires at least 3 files be" + 313 " submitted. You only submitted " + sourceFiles.size() + " files"; 314 throw new IllegalStateException(s); 315 } 316 317 // Verify submission with user 318 if (verify && !verifySubmission(sourceFiles)) { 319 // User does not want to submit 320 return false; 321 } 322 323 // Timestamp 324 this.submitTime = this.currentTimeProvider.getCurrentTime(); 325 326 // Create a temporary zip file to hold the source files 327 File zipFile = makeZipFileWith(sourceFiles); 328 329 if (this.sendEmails) { 330 // Send the zip file as an email attachment to the TA 331 mailTA(zipFile, sourceFiles); 332 333 if (sendReceipt) { 334 mailReceipt(sourceFiles); 335 } 336 } 337 338 return true; 339 } 340 341 private void askForEstimatedHours() { 342 out.println("\nTo help inform how future students plan their approach to this project, you can"); 343 out.println("record the approximate number of hours you spent on this project. This estimate"); 344 out.println("is completely optional, is used for informational purposes only, has no impact"); 345 out.println("on your grade, and will only be shared with others as part of an aggregate"); 346 out.println("summary report.\n"); 347 348 while (true) { 349 out.print("About how many hours did you spend on this project? "); 350 out.flush(); 351 352 try { 353 String line = in.readLine(); 354 if (line == null || line.isEmpty()) { 355 return; 356 } 357 358 try { 359 double estimatedHours = Double.parseDouble(line.trim()); 360 this.setEstimatedHours(estimatedHours); 361 return; 362 363 } catch (NumberFormatException ex) { 364 out.println("** Please enter a valid number or a blank line to opt out"); 365 } 366 367 } catch (IOException ex) { 368 err.println("** Exception while reading from System.in: " + ex); 369 } 370 } 371 372 } 373 374 /** 375 * Prints debugging output. 376 */ 377 private void db(String s) { 378 if (this.debug) { 379 err.println("++ " + s); 380 } 381 } 382 383 /** 384 * Searches for the files given on the command line. Ignores files 385 * that do not end in .java, or that appear on the "no submit" list. 386 * Files must reside in a directory named 387 * edu/pdx/cs410J/<studentId>. 388 */ 389 private Set<File> searchForSourceFiles(Set<String> fileNames) { 390 // Files should be sorted by name 391 SortedSet<File> files = 392 new TreeSet<>(Comparator.comparing(File::toString)); 393 populateWithFilesFromSubdirectories(files, fileNames); 394 395 files.removeIf((file) -> !canBeSubmitted(file)); 396 397 return files; 398 } 399 400 private void populateWithFilesFromSubdirectories(SortedSet<File> allFiles, Set<String> fileNames) { 401 populateWithFilesFromSubdirectories(allFiles, fileNames.stream().map(name -> new File(name).getAbsoluteFile())); 402 } 403 404 private void populateWithFilesFromSubdirectories(SortedSet<File> allFiles, Stream<File> files) { 405 files.forEach((file) -> { 406 if (file.isDirectory()) { 407 populateWithFilesFromSubdirectories(allFiles, Arrays.stream(Objects.requireNonNull(file.listFiles()))); 408 409 } else { 410 allFiles.add(file); 411 } 412 }); 413 414 } 415 416 protected boolean canBeSubmitted(File file) { 417 if (!fileExists(file)) { 418 return false; 419 } 420 421 // Is the file on the "no submit" list? 422 List<String> noSubmit = fetchListOfFilesThatCanNotBeSubmitted(); 423 String name = file.getName(); 424 if (noSubmit.contains(name)) { 425 err.println("** Not submitting file " + file + 426 " because it is on the \"no submit\" list"); 427 return false; 428 } 429 430 // Verify that file is in the correct directory. 431 if (isInAKoansDirectory(file)) { 432 if (!file.getName().endsWith(".java")) { 433 err.println("** Not submitting file " + file + 434 " because does end in \".java\""); 435 return false; 436 } 437 438 } else { 439 if (!isInMavenProjectDirectory(file)) { 440 err.println("** Not submitting file " + file + 441 ": it does not reside in a Maven project in a directory named " + 442 "edu" + File.separator + "pdx" + File.separator + 443 "cs410J" + File.separator + userId + " (or in one of the koans directories)"); 444 return false; 445 } 446 447 // Does the file name end in .java? 448 if (!canFileBeSubmitted(file)) { 449 String fileName = file.getName(); 450 int index = fileName.lastIndexOf('.'); 451 if (index < 0) { 452 err.println("** Not submitting file " + file + 453 " because it does not have an extension."); 454 455 } else { 456 String fileExtension = fileName.substring(index); 457 err.println("** Not submitting file " + file + 458 " because files ending in " + fileExtension + 459 " are not supposed to be in that directory"); 460 } 461 return false; 462 } 463 } 464 return true; 465 } 466 467 @VisibleForTesting 468 static boolean canFileBeSubmitted(File file) { 469 String path = file.getPath(); 470 471 if (path.matches(".*src/.*/java/.*")) { 472 return path.endsWith(".java"); 473 474 } else if (path.matches(".*src/.*/javadoc/.*")) { 475 return path.endsWith(".html"); 476 477 } else if (path.matches(".*src/.*/resources/.*")) { 478 return path.endsWith(".xml") || path.endsWith(".txt"); 479 480 } else { 481 return false; 482 } 483 } 484 485 boolean fileExists(File file) { 486 if (!file.exists()) { 487 err.println("** Not submitting file " + file + 488 " because it does not exist"); 489 return false; 490 } 491 return true; 492 } 493 494 private boolean isInAKoansDirectory(File file) { 495 boolean isInAKoansDirectory = hasParentDirectories(file, "beginner") || 496 hasParentDirectories(file, "intermediate") || 497 hasParentDirectories(file, "advanced") || 498 hasParentDirectories(file, "java7") || 499 hasParentDirectories(file, "java8"); 500 if (isInAKoansDirectory) { 501 this.isSubmittingKoans = true; 502 db(file + " is in a koans directory"); 503 } 504 return isInAKoansDirectory; 505 } 506 507 @VisibleForTesting 508 boolean isInMavenProjectDirectory(File file) { 509 boolean isInMavenProjectDirectory = hasParentDirectories(file, userId, "cs410J", "pdx", "edu", "java|javadoc|resources", "main|test|it", "src"); 510 if (isInMavenProjectDirectory) { 511 db(file + " is in the edu/pdx/cs410J directory"); 512 } 513 return isInMavenProjectDirectory; 514 } 515 516 private boolean hasParentDirectories(File file, String... parentDirectoryNames) { 517 File parent = file.getParentFile(); 518 519 // Skip over subpackages 520 while (parent != null && !parent.getName().equals(parentDirectoryNames[0])) { 521 parent = parent.getParentFile(); 522 } 523 524 for (String parentDirectoryName : parentDirectoryNames) { 525 if (parent == null || !parent.getName().matches(parentDirectoryName)) { 526 return false; 527 528 } else { 529 parent = parent.getParentFile(); 530 } 531 } 532 533 return true; 534 } 535 536 private List<String> fetchListOfFilesThatCanNotBeSubmitted() { 537 return fetchListOfStringsFromResource("no-submit"); 538 } 539 540 /** 541 * Prints a summary of what is about to be submitted and prompts the 542 * user to verify that it is correct. 543 * 544 * @return <code>true</code> if the user wants to submit 545 */ 546 private boolean verifySubmission(Set<File> sourceFiles) { 547 // Print out what is going to be submitted 548 out.print("\n" + userName); 549 out.print("'s submission for "); 550 out.println(projName); 551 552 for (File file : sourceFiles) { 553 out.println(" " + file); 554 } 555 556 if (comment != null) { 557 out.println("\nComment: " + comment + "\n\n"); 558 } 559 560 out.println("A receipt will be sent to: " + userEmail + "\n"); 561 562 warnIfMainProjectClassIsNotSubmitted(sourceFiles); 563 564 warnIfTestClassesAreNotSubmitted(sourceFiles); 565 566 askForEstimatedHours(); 567 568 return doesUserWantToSubmit(); 569 } 570 571 protected void warnIfMainProjectClassIsNotSubmitted(Set<File> sourceFiles) { 572 boolean wasMainProjectClassSubmitted = sourceFiles.stream().anyMatch((f) -> f.getName().contains(this.projName)); 573 if (!wasMainProjectClassSubmitted && !this.isSubmittingKoans) { 574 String mainProjectClassName = this.projName + ".java"; 575 out.println("*** WARNING: You are submitting " + this.projName + 576 ", but did not include " + mainProjectClassName + ".\n" + 577 " You might want to check the name of the project or the files you are submitting.\n"); 578 } 579 } 580 581 protected void warnIfTestClassesAreNotSubmitted(Set<File> sourceFiles) { 582 boolean wereTestClassessSubmitted = submittedTestClasses(sourceFiles); 583 if (!wereTestClassessSubmitted && !this.isSubmittingKoans) { 584 out.println("*** WARNING: You are not submitting a \"test\" directory.\n" + 585 " Your unit tests are executed as part of the grading of your project.\n"); 586 } 587 } 588 589 @VisibleForTesting 590 static boolean submittedTestClasses(Set<File> sourceFiles) { 591 return sourceFiles.stream().anyMatch((f) -> f.getPath().contains("test")); 592 } 593 594 private boolean doesUserWantToSubmit() { 595 while (true) { 596 out.print("\nDo you wish to continue with the submission? (yes/no) "); 597 out.flush(); 598 599 try { 600 String line = in.readLine().trim(); 601 switch (line) { 602 case "yes": 603 return true; 604 605 case "no": 606 return false; 607 608 default: 609 err.println("** Please enter yes or no"); 610 break; 611 } 612 613 } catch (IOException ex) { 614 err.println("** Exception while reading from System.in: " + ex); 615 } 616 } 617 } 618 619 /** 620 * Returns the name of a <code>File</code> relative to the source 621 * directory. 622 */ 623 protected String getZipEntryNameFor(File file) { 624 if (isSubmittingKoans) { 625 return file.getParentFile().getName() + "/" + file.getName(); 626 627 } else { 628 // We already know that the file is in the correct directory 629 return getZipEntryNameFor(file.getPath()); 630 } 631 } 632 633 @VisibleForTesting 634 static String getZipEntryNameFor(String filePath) { 635 Pattern pattern = Pattern.compile(".*/src/(main|test|it)/(java|javadoc|resources)/edu/pdx/cs410J/(.*)"); 636 Matcher matcher = pattern.matcher(filePath); 637 638 if (matcher.matches()) { 639 return "src/" + matcher.group(1) + "/" + matcher.group(2) + "/edu/pdx/cs410J/" + matcher.group(3); 640 } else { 641 throw new IllegalStateException("Can't extract zip entry name for " + filePath); 642 } 643 } 644 645 /** 646 * Creates a Zip file that contains the source files. The Zip File 647 * is temporary and is deleted when the program exits. 648 */ 649 private File makeZipFileWith(Set<File> sourceFiles) throws IOException { 650 String zipFileName = userName.replace(' ', '_') + "-TEMP"; 651 File zipFile = File.createTempFile(zipFileName, ".zip"); 652 if (!saveZip) { 653 zipFile.deleteOnExit(); 654 655 } else { 656 out.println("Saving temporary Zip file: " + zipFile); 657 } 658 659 db("Created Zip file: " + zipFile); 660 661 Map<File, String> sourceFilesWithNames = 662 sourceFiles.stream().collect(Collectors.toMap(file -> file, this::getZipEntryNameFor)); 663 664 new ZipFileOfFilesMaker(sourceFilesWithNames, zipFile, getManifestEntries()).makeZipFile(); 665 return zipFile; 666 } 667 668 private Map<Attributes.Name, String> getManifestEntries() { 669 Map<Attributes.Name, String> manifestEntries = new HashMap<>(); 670 manifestEntries.put(ManifestAttributes.USER_NAME, userName); 671 manifestEntries.put(ManifestAttributes.USER_ID, userId); 672 manifestEntries.put(ManifestAttributes.USER_EMAIL, userEmail); 673 manifestEntries.put(ManifestAttributes.PROJECT_NAME, projName); 674 manifestEntries.put(ManifestAttributes.SUBMISSION_COMMENT, comment); 675 manifestEntries.put(ManifestAttributes.SUBMISSION_TIME, ManifestAttributes.formatSubmissionTime(submitTime)); 676 if (estimatedHours != null) { 677 manifestEntries.put(ManifestAttributes.ESTIMATED_HOURS, String.valueOf(estimatedHours)); 678 } 679 return manifestEntries; 680 } 681 682 /** 683 * Sends the Zip file to the TA as a MIME attachment. Also includes 684 * a textual summary of the contents of the Zip file. 685 */ 686 private void mailTA(File zipFile, Set<File> sourceFiles) throws MessagingException { 687 MimeMessage message = 688 newEmailTo(newEmailSession(debug), TA_EMAIL) 689 .from(userEmail, userName) 690 .withSubject("CS410J-SUBMIT " + userName + "'s " + projName) 691 .createMessage(); 692 693 MimeBodyPart textPart = createTextPartOfTAEmail(sourceFiles); 694 MimeBodyPart filePart = createZipAttachment(zipFile); 695 696 Multipart mp = new MimeMultipart(); 697 mp.addBodyPart(textPart); 698 mp.addBodyPart(filePart); 699 700 message.setContent(mp); 701 702 out.println("Submitting project to Grader"); 703 704 Transport.send(message); 705 } 706 707 private MimeBodyPart createZipAttachment(File zipFile) throws MessagingException { 708 // Now attach the Zip file 709 DataSource ds = new FileDataSource(zipFile) { 710 @Override 711 public String getContentType() { 712 return "application/zip"; 713 } 714 }; 715 DataHandler dh = new DataHandler(ds); 716 MimeBodyPart filePart = new MimeBodyPart(); 717 718 String zipFileTitle = getZipFileTitle(); 719 720 filePart.setDataHandler(dh); 721 filePart.setFileName(zipFileTitle); 722 filePart.setDescription(userName + "'s " + projName); 723 724 return filePart; 725 } 726 727 @VisibleForTesting 728 String getZipFileTitle() { 729 return userName.replace(' ', '_') + ".zip"; 730 } 731 732 private MimeBodyPart createTextPartOfTAEmail(Set<File> sourceFiles) throws MessagingException { 733 // Create the text portion of the message 734 StringBuilder text = new StringBuilder(); 735 text.append("Student name: ").append(userName).append(" (").append(userEmail).append(")\n"); 736 text.append("Project name: ").append(projName).append("\n"); 737 text.append("Submitted on: ").append(humanReadableSubmitDate()).append("\n"); 738 if (comment != null) { 739 text.append("\nComment: ").append(comment).append("\n\n"); 740 } 741 text.append("Contents:\n"); 742 743 for (File file : sourceFiles) { 744 text.append(" ").append(getZipEntryNameFor(file)).append("\n"); 745 } 746 text.append("\n\n"); 747 748 MimeBodyPart textPart = new MimeBodyPart(); 749 textPart.setContent(text.toString(), "text/plain"); 750 751 // Try not to display text as separate attachment 752 textPart.setDisposition("inline"); 753 return textPart; 754 } 755 756 private String humanReadableSubmitDate() { 757 return submitTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)); 758 } 759 760 /** 761 * Sends a email to the user as a receipt of the submission. 762 */ 763 private void mailReceipt(Set<File> sourceFiles) throws MessagingException { 764 String subject = "CS410J " + projName + " submission"; 765 InternetAddress email = newInternetAddress(this.userEmail, this.userName); 766 MimeMessage message = 767 newEmailTo(newEmailSession(debug), email) 768 .from(TA_EMAIL) 769 .replyTo(DAVE_EMAIL) 770 .withSubject(subject) 771 .createMessage(); 772 773 // Create the contents of the message 774 StringBuilder text = new StringBuilder(); 775 text.append("On ").append(humanReadableSubmitDate()).append("\n"); 776 text.append(userName).append(" (").append(userEmail).append(")\n"); 777 text.append("submitted the following files for ").append(projName).append(":\n"); 778 779 for (File file : sourceFiles) { 780 text.append(" ").append(file.getAbsolutePath()).append("\n"); 781 } 782 783 if (comment != null) { 784 text.append("\nComment: ").append(comment).append("\n\n"); 785 } 786 787 text.append("\n\n"); 788 text.append("Have a nice day."); 789 790 // Add the text to the message and send it 791 message.setText(text.toString()); 792 message.setDisposition("inline"); 793 794 out.println("Sending receipt to you at " + userEmail); 795 796 Transport.send(message); 797 } 798 799 ///////////////////////// Main Program /////////////////////////// 800 801 /** 802 * Prints usage information about this program. 803 */ 804 protected void usage(String message) { 805 err.println("\n** " + message + "\n"); 806 err.println("usage: java " + this.getClass().getSimpleName() + " [options] args file+"); 807 err.println(" args are (in this order):"); 808 err.println(" project What project is being submitted (Project1, Project2, etc.)"); 809 err.println(" studentXmlFile XML file with info about who is submitting the project"); 810 err.println(" srcDirectory Directory containing source code to submit"); 811 err.println(" options are (options may appear in any order):"); 812 err.println(" -savezip Saves temporary Zip file"); 813 err.println(" -nosend Generates zip file, but does not send emails"); 814 err.println(" -smtp serverName Name of SMTP server"); 815 err.println(" -verbose Log debugging output"); 816 err.println(" -comment comment Info for the Grader"); 817 err.println(""); 818 err.println("Submits Java source code to the CS410J grader."); 819 System.exit(1); 820 } 821 822 public static void main(String[] args) throws IOException, MessagingException { 823 Submit submit = new Submit(); 824 submit.parseCommandLineAndSubmit(args); 825 } 826 827 /** 828 * Parses the command line, finds the source files, prompts the user 829 * to verify whether or not the settings are correct, and then sends 830 * an email to the Grader. 831 */ 832 void parseCommandLineAndSubmit(String[] args) throws IOException, MessagingException { 833 // Parse the command line 834 for (int i = 0; i < args.length; i++) { 835 // Check for options first 836 if (args[i].equals("-smtp")) { 837 if (++i >= args.length) { 838 usage("No SMTP server specified"); 839 } 840 841 this.setEmailServerHostName(args[i]); 842 843 } else if (args[i].equals("-verbose")) { 844 this.setDebug(true); 845 846 } else if (args[i].equals("-savezip")) { 847 this.saveZip(); 848 849 } else if (args[i].equals("-nosend")) { 850 this.doNotSendEmails(); 851 852 } else if (args[i].equals("-comment")) { 853 if (++i >= args.length) { 854 usage("No comment specified"); 855 } 856 857 this.setComment(args[i]); 858 859 } else if (this.projName == null) { 860 this.setProjectName(args[i]); 861 862 } else if (this.studentXmlFileName == null) { 863 this.studentXmlFileName = args[i]; 864 865 } else { 866 // The name of a source file 867 this.addFile(args[i]); 868 } 869 } 870 871 boolean submitted; 872 873 try { 874 // Make sure that user entered enough information 875 this.validate(); 876 877 this.db("Command line successfully parsed."); 878 879 submitted = this.submit(true); 880 881 } catch (IllegalStateException ex) { 882 usage(ex.getMessage()); 883 return; 884 } 885 886 // All done. 887 if (submitted) { 888 out.println(this.projName + " submitted successfully. Thank you."); 889 890 } else { 891 out.println(this.projName + " not submitted."); 892 } 893 } 894 895 void setSendReceipt(boolean sendReceipt) { 896 this.sendReceipt = sendReceipt; 897 898 } 899 900 public void setEstimatedHours(Double estimatedHours) { 901 this.estimatedHours = estimatedHours; 902 } 903 904 static class ManifestAttributes { 905 906 static final Attributes.Name USER_NAME = new Attributes.Name("Submitter-User-Name"); 907 static final Attributes.Name USER_ID = new Attributes.Name("Submitter-User-Id"); 908 static final Attributes.Name USER_EMAIL = new Attributes.Name("Submitter-Email"); 909 static final Attributes.Name PROJECT_NAME = new Attributes.Name("Project-Name"); 910 static final Attributes.Name SUBMISSION_TIME = new Attributes.Name("Submission-Time"); 911 static final Attributes.Name SUBMISSION_COMMENT = new Attributes.Name("Submission-Comment"); 912 static final Attributes.Name ESTIMATED_HOURS = new Attributes.Name("Estimated-Hours"); 913 914 private static final DateTimeFormatter LEGACY_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss"); 915 private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME; 916 917 static String formatSubmissionTime(LocalDateTime submitTime) { 918 return submitTime.format(DATE_TIME_FORMATTER); 919 } 920 921 static LocalDateTime parseSubmissionTime(String string) { 922 try { 923 return LocalDateTime.parse(string, DATE_TIME_FORMATTER); 924 925 } catch (DateTimeParseException ex) { 926 return LocalDateTime.parse(string, LEGACY_DATE_TIME_FORMATTER); 927 } 928 } 929 } 930 931 @VisibleForTesting 932 interface CurrentTimeProvider { 933 LocalDateTime getCurrentTime(); 934 } 935}