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