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}