package edu.pdx.cs410J.grader; import com.google.common.annotations.VisibleForTesting; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.activation.FileDataSource; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Transport; import javax.mail.internet.*; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.*; import java.util.jar.Attributes; import java.util.stream.Collectors; import java.util.stream.Stream; /** * This class is used to submit assignments in CS410J. The user * specified his or her email address as well as the base directory * for his/her source files on the command line. The directory is * searched recursively for files ending in .java. Those files are * placed in a zip file and emailed to the grader. A confirmation * email is sent to the submitter. * * More information about the JavaMail API can be found at: * * <center><a href="http://java.sun.com/products/javamail"> * http://java.sun.com/products/javamail</a></center> * * @author David Whitlock * @since Fall 2000 (Refactored to use fewer static methods in Spring 2006) */ public class Submit extends EmailSender { private static final PrintWriter out = new PrintWriter(System.out, true); private static final PrintWriter err = new PrintWriter(System.err, true); /** * The grader's email address */ @VisibleForTesting static final String TA_EMAIL = "sjavata@gmail.com"; /** * A URL containing a list of files that should not be submitted */ private static final String NO_SUBMIT_LIST_URL = "http://www.cs.pdx.edu/~whitlock/no-submit"; private static final String PROJECT_NAMES_LIST_URL = "http://www.cs.pdx.edu/~whitlock/project-names"; ///////////////////// Instance Fields ////////////////////////// /** * The name of the project being submitted */ private String projName = null; /** * The name of the user (student) submits the project */ private String userName = null; /** * The submitter's email address */ private String userEmail = null; /** * The submitter's user id */ private String userId = null; /** * A comment describing the project */ private String comment = null; /** * Should the execution of this program be logged? */ private boolean debug = false; /** * Should the generated zip file be saved? */ private boolean saveZip = false; /** * The time at which the project was submitted */ private LocalDateTime submitTime = null; /** * The names of the files to be submitted */ private Set<String> fileNames = new HashSet<>(); private boolean isSubmittingKoans = false; private boolean sendReceipt = true; /////////////////////// Constructors ///////////////////////// /** * Creates a new <code>Submit</code> program */ public Submit() { } ///////////////////// Instance Methods /////////////////////// /** * Sets the name of the SMTP server that is used to send emails */ public void setEmailServerHostName(String serverName) { EmailSender.serverName = serverName; } /** * Sets whether or not the progress of the submission should be logged. */ public void setDebug(boolean debug) { this.debug = debug; } /** * Sets whether or not the zip file generated by the submission * should be saved. */ public void setSaveZip(boolean saveZip) { this.saveZip = saveZip; } /** * Sets the comment for this submission */ public void setComment(String comment) { this.comment = comment; } /** * Sets the name of project being submitted */ public void setProjectName(String projName) { this.projName = projName; } /** * Sets the name of the user who is submitting the project */ public void setUserName(String userName) { this.userName = userName; } /** * Sets the id of the user who is submitting the project */ public void setUserId(String userId) { this.userId = userId; } /** * Sets the email address of the user who is submitting the project */ public void setUserEmail(String userEmail) { this.userEmail = userEmail; } /** * Adds the file with the given name to the list of files to be * submitted. */ public void addFile(String fileName) { this.fileNames.add(fileName); } /** * Validates the state of this submission * * @throws IllegalStateException If any state is incorrect or missing */ public void validate() { validateProjectName(); if (projName == null) { throw new IllegalStateException("Missing project name"); } if (userName == null) { throw new IllegalStateException("Missing student name"); } if (userId == null) { throw new IllegalStateException("Missing login id"); } if (isNineDigitStudentId(userId)) { throw new IllegalStateException(loginIdShouldNotBeStudentId(userId)); } if (looksLikeAnEmailAddress(userId)) { throw new IllegalStateException(loginIdShouldNotBeEmailAddress(userId)); } if (userEmail == null) { throw new IllegalStateException("Missing email address"); } else { // Make sure user's email is okay try { new InternetAddress(userEmail); } catch (AddressException ex) { String s = "Invalid email address: " + userEmail; IllegalStateException ex2 = new IllegalStateException(s); ex2.initCause(ex); throw ex2; } } } private String loginIdShouldNotBeEmailAddress(String userId) { return "You login id (" + userId + ") should not be an email address"; } @VisibleForTesting boolean looksLikeAnEmailAddress(String userId) { return userId.contains("@"); } @VisibleForTesting boolean isNineDigitStudentId(String userId) { return userId.matches("^[0-9]{9}$"); } private String loginIdShouldNotBeStudentId(String userId) { return "Your login id (" + userId + ") should not be your 9-digit student id"; } private void validateProjectName() { List<String> validProjectNames = fetchListOfValidProjectNames(); if (!validProjectNames.contains(projName)) { String message = "\"" + projName + "\" is not in the list of valid project names: " + validProjectNames; throw new IllegalStateException(message); } } private List<String> fetchListOfValidProjectNames() { return fetchListOfStringsFromUrl(PROJECT_NAMES_LIST_URL); } /** * Submits the project to the grader * * @param verify Should the user be prompted to verify his submission? * @return Whether or not the submission actually occurred * @throws IllegalStateException If no source files were found */ public boolean submit(boolean verify) throws IOException, MessagingException { // Recursively search the source directory for .java files Set<File> sourceFiles = searchForSourceFiles(fileNames); db(sourceFiles.size() + " source files found"); if (sourceFiles.size() == 0) { String s = "No source files were found."; throw new IllegalStateException(s); } else if (sourceFiles.size() < 3) { String s = "Too few source files were submitted. Each project requires at least 3 files be" + " submitted. You only submitted " + sourceFiles.size() + " files"; throw new IllegalStateException(s); } // Verify submission with user if (verify && !verifySubmission(sourceFiles)) { // User does not want to submit return false; } // Timestamp this.submitTime = LocalDateTime.now(); // Create a temporary zip file to hold the source files File zipFile = makeZipFileWith(sourceFiles); // Send the zip file as an email attachment to the TA mailTA(zipFile, sourceFiles); if (sendReceipt) { mailReceipt(sourceFiles); } return true; } /** * Prints debugging output. */ private void db(String s) { if (this.debug) { err.println("++ " + s); } } /** * Searches for the files given on the command line. Ignores files * that do not end in .java, or that appear on the "no submit" list. * Files must reside in a directory named * edu/pdx/cs410J/<studentId>. */ private Set<File> searchForSourceFiles(Set<String> fileNames) { // Files should be sorted by name SortedSet<File> files = new TreeSet<>((o1, o2) -> o1.toString().compareTo(o2.toString())); populateWithFilesFromSubdirectories(files, fileNames); files.removeIf((file) -> !canBeSubmitted(file)); return files; } private void populateWithFilesFromSubdirectories(SortedSet<File> allFiles, Set<String> fileNames) { populateWithFilesFromSubdirectories(allFiles, fileNames.stream().map(name -> new File(name).getAbsoluteFile())); } private void populateWithFilesFromSubdirectories(SortedSet<File> allFiles, Stream<File> files) { files.forEach((file) -> { if (file.isDirectory()) { populateWithFilesFromSubdirectories(allFiles, Arrays.stream(file.listFiles())); } else { allFiles.add(file); } }); } private boolean canBeSubmitted(File file) { if (!file.exists()) { err.println("** Not submitting file " + file + " because it does not exist"); return false; } // Is the file on the "no submit" list? List<String> noSubmit = fetchListOfFilesThatCanNotBeSubmitted(); String name = file.getName(); if (noSubmit.contains(name)) { err.println("** Not submitting file " + file + " because it is on the \"no submit\" list"); return false; } // Does the file name end in .java? if (!name.endsWith(".java")) { err.println("** Not submitting file " + file + " because does end in \".java\""); return false; } // Verify that file is in the correct directory. if (!isInAKoansDirectory(file) && !isInEduPdxCs410JDirectory(file)) { err.println("** Not submitting file " + file + ": it does not reside in a directory named " + "edu" + File.separator + "pdx" + File.separator + "cs410J" + File.separator + userId + " (or in one of the koans directories)"); return false; } return true; } private boolean isInAKoansDirectory(File file) { boolean isInAKoansDirectory = hasParentDirectories(file, "beginner") || hasParentDirectories(file, "intermediate") || hasParentDirectories(file, "advanced") || hasParentDirectories(file, "java7") || hasParentDirectories(file, "java8"); if (isInAKoansDirectory) { this.isSubmittingKoans = true; db(file + " is in a koans directory"); } return isInAKoansDirectory; } @VisibleForTesting boolean isInEduPdxCs410JDirectory(File file) { boolean isInEduPdxCs410JDirectory = hasParentDirectories(file, userId, "cs410J", "pdx", "edu"); if (isInEduPdxCs410JDirectory) { db(file + " is in the edu/pdx/cs410J directory"); } return isInEduPdxCs410JDirectory; } private boolean hasParentDirectories(File file, String... parentDirectoryNames) { File parent = file.getParentFile(); // Skip over subpackages while (parent != null && !parent.getName().equals(parentDirectoryNames[0])) { parent = parent.getParentFile(); } for (String parentDirectoryName : parentDirectoryNames) { if (parent == null || !parent.getName().equals(parentDirectoryName)) { return false; } else { parent = parent.getParentFile(); } } return true; } private List<String> fetchListOfFilesThatCanNotBeSubmitted() { return fetchListOfStringsFromUrl(NO_SUBMIT_LIST_URL); } private List<String> fetchListOfStringsFromUrl(String listUrl) { List<String> noSubmit = new ArrayList<>(); try { URL url = new URL(listUrl); InputStreamReader isr = new InputStreamReader(url.openStream()); BufferedReader br = new BufferedReader(isr); while (br.ready()) { noSubmit.add(br.readLine().trim()); } } catch (MalformedURLException ex) { err.println("** WARNING: Cannot access " + listUrl + ": " + ex.getMessage()); } catch (IOException ex) { err.println("** WARNING: Problems while reading " + listUrl + ": " + ex.getMessage()); } return noSubmit; } /** * Prints a summary of what is about to be submitted and prompts the * user to verify that it is correct. * * @return <code>true</code> if the user wants to submit */ private boolean verifySubmission(Set<File> sourceFiles) { // Print out what is going to be submitted out.print("\n" + userName); out.print("'s submission for "); out.println(projName); for (File file : sourceFiles) { out.println(" " + file); } if (comment != null) { out.println("\nComment: " + comment + "\n\n"); } out.println("A receipt will be sent to: " + userEmail + "\n"); warnIfMainProjectClassIsNotSubmitted(sourceFiles); return doesUserWantToSubmit(); } private void warnIfMainProjectClassIsNotSubmitted(Set<File> sourceFiles) { boolean wasMainProjectClassSubmitted = sourceFiles.stream().anyMatch((f) -> f.getName().contains(this.projName)); if (!wasMainProjectClassSubmitted && !this.isSubmittingKoans) { String mainProjectClassName = this.projName + ".java"; out.println("*** WARNING: You are submitting " + this.projName + ", but did not include " + mainProjectClassName + ".\n" + " You might want to check the name of the project or the files you are submitting.\n"); } } private boolean doesUserWantToSubmit() { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader in = new BufferedReader(isr); while (true) { out.print("Do you wish to continue with the submission? (yes/no) "); out.flush(); try { String line = in.readLine().trim(); switch (line) { case "yes": return true; case "no": return false; default: err.println("** Please enter yes or no"); break; } } catch (IOException ex) { err.println("** Exception while reading from System.in: " + ex); } } } /** * Returns the name of a <code>File</code> relative to the source * directory. */ private String getRelativeName(File file) { if (isSubmittingKoans) { return file.getParentFile().getName() + "/" + file.getName(); } else { // We already know that the file is in the correct directory return "edu/pdx/cs410J/" + userId + "/" + file.getName(); } } /** * Creates a Zip file that contains the source files. The Zip File * is temporary and is deleted when the program exits. */ private File makeZipFileWith(Set<File> sourceFiles) throws IOException { String zipFileName = userName.replace(' ', '_') + "-TEMP"; File zipFile = File.createTempFile(zipFileName, ".zip"); if (!saveZip) { zipFile.deleteOnExit(); } else { out.println("Saving temporary Zip file: " + zipFile); } db("Created Zip file: " + zipFile); Map<File, String> sourceFilesWithNames = sourceFiles.stream().collect(Collectors.toMap(file -> file, this::getRelativeName)); return new ZipMaker(sourceFilesWithNames, zipFile, getManifestEntries()).makeZipFile(); } private Map<Attributes.Name, String> getManifestEntries() { Map<Attributes.Name, String> manifestEntries = new HashMap<>(); manifestEntries.put(ManifestAttributes.USER_NAME, userName); manifestEntries.put(ManifestAttributes.USER_ID, userId); manifestEntries.put(ManifestAttributes.USER_EMAIL, userEmail); manifestEntries.put(ManifestAttributes.PROJECT_NAME, projName); manifestEntries.put(ManifestAttributes.SUBMISSION_COMMENT, comment); manifestEntries.put(ManifestAttributes.SUBMISSION_TIME, ManifestAttributes.formatSubmissionTime(submitTime)); return manifestEntries; } /** * Sends the Zip file to the TA as a MIME attachment. Also includes * a textual summary of the contents of the Zip file. */ private void mailTA(File zipFile, Set<File> sourceFiles) throws MessagingException { MimeMessage message = newEmailTo(newEmailSession(debug), TA_EMAIL, "CS410J-SUBMIT " + userName + "'s " + projName); MimeBodyPart textPart = createTextPartOfTAEmail(sourceFiles); MimeBodyPart filePart = createZipAttachment(zipFile); Multipart mp = new MimeMultipart(); mp.addBodyPart(textPart); mp.addBodyPart(filePart); message.setContent(mp); out.println("Submitting project to Grader"); Transport.send(message); } private MimeBodyPart createZipAttachment(File zipFile) throws MessagingException { // Now attach the Zip file DataSource ds = new FileDataSource(zipFile) { @Override public String getContentType() { return "application/zip"; } }; DataHandler dh = new DataHandler(ds); MimeBodyPart filePart = new MimeBodyPart(); String zipFileTitle = userName.replace(' ', '_') + ".zip"; filePart.setDataHandler(dh); filePart.setFileName(zipFileTitle); filePart.setDescription(userName + "'s " + projName); return filePart; } private MimeBodyPart createTextPartOfTAEmail(Set<File> sourceFiles) throws MessagingException { // Create the text portion of the message StringBuilder text = new StringBuilder(); text.append("Student name: ").append(userName).append(" (").append(userEmail).append(")\n"); text.append("Project name: ").append(projName).append("\n"); text.append("Submitted on: ").append(humanReadableSubmitDate()).append("\n"); if (comment != null) { text.append("\nComment: ").append(comment).append("\n\n"); } text.append("Contents:\n"); for (File file : sourceFiles) { text.append(" ").append(getRelativeName(file)).append("\n"); } text.append("\n\n"); MimeBodyPart textPart = new MimeBodyPart(); textPart.setContent(text.toString(), "text/plain"); // Try not to display text as separate attachment textPart.setDisposition("inline"); return textPart; } private String humanReadableSubmitDate() { return submitTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)); } /** * Sends a email to the user as a receipt of the submission. */ private void mailReceipt(Set<File> sourceFiles) throws MessagingException { MimeMessage message = newEmailTo(newEmailSession(debug), userEmail, "CS410J " + projName + " submission"); // Create the contents of the message StringBuilder text = new StringBuilder(); text.append("On ").append(humanReadableSubmitDate()).append("\n"); text.append(userName).append(" (").append(userEmail).append(")\n"); text.append("submitted the following files for ").append(projName).append(":\n"); for (File file : sourceFiles) { text.append(" ").append(file.getAbsolutePath()).append("\n"); } if (comment != null) { text.append("\nComment: ").append(comment).append("\n\n"); } text.append("\n\n"); text.append("Have a nice day."); // Add the text to the message and send it message.setText(text.toString()); message.setDisposition("inline"); out.println("Sending receipt to you at " + userEmail); Transport.send(message); } ///////////////////////// Main Program /////////////////////////// /** * Prints usage information about this program. */ private static void usage(String s) { err.println("\n** " + s + "\n"); err.println("usage: java Submit [options] args file+"); err.println(" args are (in this order):"); err.println(" project What project is being submitted (Project1, Project2, etc.)"); err.println(" student Who is submitting the project?"); err.println(" loginId UNIX login id"); err.println(" email Student's email address"); err.println(" file Java source file (or all files in a directory) to submit"); err.println(" options are (options may appear in any order):"); err.println(" -savezip Saves temporary Zip file"); err.println(" -smtp serverName Name of SMTP server"); err.println(" -verbose Log debugging output"); err.println(" -comment comment Info for the Grader"); err.println(""); err.println("Submits Java source code to the CS410J grader."); System.exit(1); } /** * Parses the command line, finds the source files, prompts the user * to verify whether or not the settings are correct, and then sends * an email to the Grader. */ public static void main(String[] args) throws IOException, MessagingException { Submit submit = new Submit(); // Parse the command line for (int i = 0; i < args.length; i++) { // Check for options first if (args[i].equals("-smtp")) { if (++i >= args.length) { usage("No SMTP server specified"); } submit.setEmailServerHostName(args[i]); } else if (args[i].equals("-verbose")) { submit.setDebug(true); } else if (args[i].equals("-savezip")) { submit.setSaveZip(true); } else if (args[i].equals("-comment")) { if (++i >= args.length) { usage("No comment specified"); } submit.setComment(args[i]); } else if (submit.projName == null) { submit.setProjectName(args[i]); } else if (submit.userName == null) { submit.setUserName(args[i]); } else if (submit.userId == null) { submit.setUserId(args[i]); } else if (submit.userEmail == null) { submit.setUserEmail(args[i]); } else { // The name of a source file submit.addFile(args[i]); } } boolean submitted; try { // Make sure that user entered enough information submit.validate(); submit.db("Command line successfully parsed."); submitted = submit.submit(true); } catch (IllegalStateException ex) { usage(ex.getMessage()); return; } // All done. if (submitted) { out.println(submit.projName + " submitted successfully. Thank you."); } else { out.println(submit.projName + " not submitted."); } } public void setSendReceipt(boolean sendReceipt) { this.sendReceipt = sendReceipt; } static class ManifestAttributes { public static final Attributes.Name USER_NAME = new Attributes.Name("Submitter-User-Name"); public static final Attributes.Name USER_ID = new Attributes.Name("Submitter-User-Id"); public static final Attributes.Name USER_EMAIL = new Attributes.Name("Submitter-Email"); public static final Attributes.Name PROJECT_NAME = new Attributes.Name("Project-Name"); public static final Attributes.Name SUBMISSION_TIME = new Attributes.Name("Submission-Time"); public static final Attributes.Name SUBMISSION_COMMENT = new Attributes.Name("Submission-Comment"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss"); public static String formatSubmissionTime(LocalDateTime submitTime) { return submitTime.format(DATE_TIME_FORMATTER); } public static LocalDateTime parseSubmissionTime(String string) { return LocalDateTime.parse(string, DATE_TIME_FORMATTER); } } }