001package edu.pdx.cs.joy.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import com.google.common.io.ByteStreams; 005import edu.pdx.cs.joy.grader.gradebook.Assignment; 006import edu.pdx.cs.joy.grader.gradebook.Grade; 007import edu.pdx.cs.joy.grader.gradebook.GradeBook; 008import edu.pdx.cs.joy.grader.gradebook.Student; 009import jakarta.mail.Message; 010 011import java.io.*; 012import java.time.LocalDateTime; 013import java.util.Collections; 014import java.util.Optional; 015import java.util.jar.Attributes; 016import java.util.jar.JarFile; 017import java.util.jar.Manifest; 018import java.util.zip.ZipEntry; 019import java.util.zip.ZipInputStream; 020 021class ProjectSubmissionsProcessor extends StudentEmailAttachmentProcessor { 022 023 @VisibleForTesting 024 static final String EMAIL_FOLDER_NAME = "Project Submissions"; 025 026 public ProjectSubmissionsProcessor(File directory, GradeBook gradeBook) { 027 super(directory, gradeBook); 028 } 029 030 @Override 031 public Iterable<? extends String> getSupportedContentTypes() { 032 return Collections.singleton("application/zip"); 033 } 034 035 @Override 036 public void processAttachment(Message message, String fileName, InputStream inputStream, String contentType) { 037 debug(" File name: " + fileName); 038 debug(" InputStream: " + inputStream); 039 040 byte[] bytes; 041 try { 042 bytes = readAttachmentIntoByteArray(inputStream); 043 044 } catch (IOException ex) { 045 logException("While copying \"" + fileName + " \" to a byte buffer", ex); 046 return; 047 } 048 049 050 Manifest manifest; 051 try { 052 manifest = getManifestFromByteArray(bytes); 053 054 } catch (IOException ex) { 055 logException("While reading jar file \"" + fileName + "\"", ex); 056 return; 057 } 058 059 try { 060 writeSubmissionToDisk(fileName, bytes, manifest); 061 062 } catch (IOException | SubmissionException ex) { 063 logException("While writing \"" + fileName + "\" to \"" + directory + "\"", ex); 064 return; 065 } 066 067 try { 068 noteSubmissionInGradeBook(manifest); 069 070 } catch (SubmissionException ex) { 071 logException("While noting submission from \"" + fileName + "\"", ex); 072 } 073 074 } 075 076 private byte[] readAttachmentIntoByteArray(InputStream inputStream) throws IOException { 077 byte[] bytes; 078 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 079 ByteStreams.copy(inputStream, baos); 080 bytes = baos.toByteArray(); 081 return bytes; 082 } 083 084 private void writeSubmissionToDisk(String fileName, byte[] bytes, Manifest manifest) throws SubmissionException, IOException { 085 File projectDir = getProjectDirectory(manifest); 086 087 File file = new File(projectDir, fileName); 088 if (file.exists()) { 089 warnOfPreExistingFile(file); 090 } 091 092 info("Writing " + fileName + " to " + projectDir); 093 094 ByteStreams.copy(new ByteArrayInputStream(bytes), new FileOutputStream(file)); 095 } 096 097 private File getProjectDirectory(Manifest manifest) throws SubmissionException, IOException { 098 String projectName = getProjectNameFromManifest(manifest.getMainAttributes()); 099 File projectDir = new File(directory, projectName); 100 if (!projectDir.exists()) { 101 if(!projectDir.mkdirs()) { 102 throw new IOException("Could not create directory: " + projectDir); 103 } 104 } 105 return projectDir; 106 } 107 108 @VisibleForTesting 109 void noteSubmissionInGradeBook(Manifest manifest) throws SubmissionException { 110 Attributes attrs = manifest.getMainAttributes(); 111 112 Student student = getStudentFromGradeBook(attrs); 113 Assignment project = getProjectFromGradeBook(attrs); 114 String note = getSubmissionNote(attrs); 115 116 Grade grade = student.getGrade(project); 117 if (grade == null) { 118 grade = new Grade(project, Grade.NO_GRADE); 119 student.setGrade(project.getName(), grade); 120 } 121 grade.addNote(note); 122 123 LocalDateTime submissionTime = getSubmissionTime(attrs); 124 Grade.SubmissionInfo submission = grade.noteSubmission(submissionTime); 125 submission.setEstimatedHours(getEstimatedHours(attrs)); 126 127 if (project.isSubmissionLate(submissionTime)) { 128 student.addLate(project.getName()); 129 submission.setIsLate(true); 130 } 131 } 132 133 public static LocalDateTime getSubmissionTime(Attributes attrs) throws SubmissionException { 134 String string = getSubmissionTimeString(attrs); 135 return Submit.ManifestAttributes.parseSubmissionTime(string); 136 } 137 138 private Double getEstimatedHours(Attributes attrs) { 139 String estimateHours = attrs.getValue(Submit.ManifestAttributes.ESTIMATED_HOURS); 140 return estimateHours == null ? null : Double.parseDouble(estimateHours); 141 } 142 143 private String getSubmissionNote(Attributes attrs) throws SubmissionException { 144 String studentName = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_NAME, "Student name missing from manifest"); 145 String submissionTime = getSubmissionTimeString(attrs); 146 String submissionComment = getManifestAttributeValue(attrs, Submit.ManifestAttributes.SUBMISSION_COMMENT, "Submission comment missing from manifest"); 147 148 return "Submitted by: " + studentName + "\n" + 149 "On: " + submissionTime + "\n" + 150 "With comment: " + submissionComment + "\n"; 151 } 152 153 private static String getSubmissionTimeString(Attributes attrs) throws SubmissionException { 154 return getManifestAttributeValue(attrs, Submit.ManifestAttributes.SUBMISSION_TIME, "Submission time missing from manifest"); 155 } 156 157 private Assignment getProjectFromGradeBook(Attributes attrs) throws SubmissionException { 158 String projectName = getProjectNameFromManifest(attrs); 159 160 Assignment assignment = this.gradeBook.getAssignment(projectName); 161 if (assignment == null) { 162 throw new SubmissionException("Assignment with name \"" + projectName + "\" is not in grade book"); 163 } 164 return assignment; 165 } 166 167 private String getProjectNameFromManifest(Attributes attrs) throws SubmissionException { 168 return getManifestAttributeValue(attrs, Submit.ManifestAttributes.PROJECT_NAME, "Project name missing from manifest"); 169 } 170 171 private Student getStudentFromGradeBook(Attributes attrs) throws SubmissionException { 172 String studentId = getStudentIdFromManifestAttributes(attrs); 173 String studentName = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_NAME, "Student Name missing from manifest"); 174 String studentEmail = getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_EMAIL, "Student Email missing from manifest"); 175 176 Optional<Student> optional = this.gradeBook.studentsStream().filter(student -> 177 hasStudentId(student, studentId) || 178 hasFirstNameLastName(student, studentName) || 179 hasNickNameLastName(student, studentName) || 180 hasEmail(student, studentEmail)).findAny(); 181 182 return optional.orElseThrow(() -> { 183 String s = "Could not find student with id \"" + studentId + "\" or name \"" + 184 studentName + "\" or email \"" + studentEmail + "\" in grade book"; 185 return new SubmissionException(s); 186 }); 187 } 188 189 public static String getStudentIdFromManifestAttributes(Attributes attrs) throws SubmissionException { 190 return getManifestAttributeValue(attrs, Submit.ManifestAttributes.USER_ID, "Student Id missing from manifest"); 191 } 192 193 private boolean hasEmail(Student student, String studentEmail) { 194 return studentEmail.equals(student.getEmail()); 195 } 196 197 private boolean hasNickNameLastName(Student student, String studentName) { 198 return studentName.equals(student.getNickName() + " " + student.getLastName()); 199 } 200 201 private boolean hasFirstNameLastName(Student student, String studentName) { 202 return studentName.equals(student.getFirstName() + " " + student.getLastName()); 203 } 204 205 private boolean hasStudentId(Student student, String studentId) { 206 return student.getId().equals(studentId); 207 } 208 209 210 private static String getManifestAttributeValue(Attributes attrs, Attributes.Name attribute, String message) throws SubmissionException { 211 String value = attrs.getValue(attribute); 212 if (value == null) { 213 throwSubmissionException(message); 214 } 215 return value; 216 } 217 218 private static void throwSubmissionException(String message) throws SubmissionException { 219 throw new SubmissionException(message); 220 } 221 222 private Manifest getManifestFromByteArray(byte[] file) throws IOException { 223 InputStream zipFile = new ByteArrayInputStream(file); 224 return getManifestFromZipFile(zipFile); 225 } 226 227 public static Manifest getManifestFromZipFile(InputStream zipFile) throws IOException { 228 ZipInputStream in = new ZipInputStream(zipFile); 229 for (ZipEntry entry = in.getNextEntry(); entry != null ; entry = in.getNextEntry()) { 230 if (entry.getName().equals(JarFile.MANIFEST_NAME)) { 231 Manifest manifest = new Manifest(); 232 manifest.read(in); 233 in.closeEntry(); 234 return manifest; 235 236 } else { 237 in.closeEntry(); 238 } 239 } 240 241 throw new IllegalStateException("Zip file did not contain manifest"); 242 } 243 244 private void warnOfPreExistingFile(File file) { 245 warn("Overwriting existing file \"" + file + "\""); 246 } 247 248 @Override 249 public String getEmailFolder() { 250 return EMAIL_FOLDER_NAME; 251 } 252 253}