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}