001package edu.pdx.cs410J.grader.gradebook;
002
003import edu.pdx.cs410J.grader.gradebook.Assignment.ProjectType;
004import org.w3c.dom.*;
005
006import javax.xml.parsers.DocumentBuilder;
007import javax.xml.parsers.DocumentBuilderFactory;
008import javax.xml.parsers.ParserConfigurationException;
009import javax.xml.transform.TransformerException;
010import java.io.File;
011import java.io.FileWriter;
012import java.io.IOException;
013import java.io.PrintWriter;
014import java.time.LocalDateTime;
015import java.util.Iterator;
016import java.util.List;
017import java.util.Objects;
018
019import static edu.pdx.cs410J.grader.gradebook.GradeBook.LetterGradeRanges.LetterGradeRange;
020
021/**
022 * This class dumps the contents of a <code>GradeBook</code> to an XML
023 * file.  By default, the students in the grade book are dumped to XML
024 * files in the same directory as the grade book's XML file.
025 *
026 * @author David Whitlock
027 * @since Fall 2000
028 */
029public class XmlDumper extends XmlHelper {
030
031  private File studentDir = null; // Where to dump student XML files
032  PrintWriter pw;          // Where to dump grade book
033
034  /**
035   * Creates a new <code>XmlDumper</code> that dumps the contents of a
036   * grade book to a given file.
037   */
038  public XmlDumper(File xmlFile) throws IOException {
039    this(new PrintWriter(new FileWriter(xmlFile), true));
040
041    if (!xmlFile.exists()) {
042      if (!xmlFile.createNewFile()) {
043        throw new IOException("Could not create file " + xmlFile);
044      }
045    }
046
047    this.setStudentDir(xmlFile.getCanonicalFile().getParentFile());
048  }
049
050  /**
051   * Creates a new <code>XmlDumper</code> that dumps the contents of a
052   * grade book to a file of the given name.
053   */
054  public XmlDumper(String xmlFileName) throws IOException {
055    this(new File(xmlFileName));
056  }
057
058  /**
059   * Creates an <code>XmlDumper</code> that dumps the contents of a
060   * grade book to a <code>PrintWriter</code>.  The location of the
061   * student files is unspecified.
062   */
063  private XmlDumper(PrintWriter pw) {
064    this.pw = pw;
065  }
066
067  /**
068   * Sets the directory in which the XML files for students are
069   * generated.
070   */
071  public void setStudentDir(File dir) {
072    if (dir.exists() && !dir.isDirectory()) {
073      throw new IllegalArgumentException(dir + " is not a directory");
074    }
075    
076    this.studentDir = dir;
077  }
078
079  /**
080   * Dumps the contents of a <code>GradeBook</code> in XML format.
081   */
082  public void dump(GradeBook book) throws IOException {
083    Document doc = dumpGradeBook(book, this);
084
085    try {
086      writeXmlToPrintWriter(doc, this.pw);
087
088    } catch (TransformerException ex) {
089      ex.printStackTrace(System.err);
090      System.exit(1);
091    }
092
093    dumpDirtyStudents(book);
094
095    // Mark the grade book as being clean
096    book.makeClean();
097  }
098
099  static Document dumpGradeBook(GradeBook book, XmlHelper helper) {
100    Document doc = createDocumentForGradeBook(helper);
101
102    Element root = doc.getDocumentElement();
103
104    appendXmlForClassName(book, root);
105    appendXmlForAssignments(book, doc, root);
106    appendXmlForLetterGradeRanges(book, doc, root);
107    appendXmlForSectionNames(book, doc, root);
108    appendXmlForStudents(book, doc, root);
109
110    return doc;
111  }
112
113  private static void appendXmlForSectionNames(GradeBook book, Document doc, Element root) {
114    for (Student.Section section : book.getSections()) {
115      String sectionName = book.getSectionName(section);
116      if (sectionName != null) {
117        root.appendChild(appendXmlForSectionName(root, section, sectionName));
118      }
119    }
120  }
121
122  private static Element appendXmlForSectionName(Element parent, Student.Section section, String sectionName) {
123    Element sectionNameNode = appendTextElementIfValueIsNotNull(parent, "section-name", sectionName);
124    sectionNameNode.setAttribute("for-section", getSectionXmlAttributeValue(section));
125    return sectionNameNode;
126  }
127
128  private static void appendXmlForLetterGradeRanges(GradeBook book, Document doc, Element root) {
129    for (Student.Section section : book.getSections()) {
130      Element lgrNode = appendXmlForLetterGradeRange(book, section, doc);
131      root.appendChild(lgrNode);
132    }
133  }
134
135  private static Element appendXmlForLetterGradeRange(GradeBook book, Student.Section section, Document doc) {
136    Element lgrNode = doc.createElement("letter-grade-ranges");
137    lgrNode.setAttribute("for-section", getSectionXmlAttributeValue(section));
138    for (LetterGradeRange range : book.getLetterGradeRanges(section)) {
139      appendXmlForLetterGradeRange(range, doc, lgrNode);
140    }
141    return lgrNode;
142  }
143
144  private static String getSectionXmlAttributeValue(Student.Section section) {
145    return Objects.toString(section, null);
146  }
147
148  private static void appendXmlForLetterGradeRange(LetterGradeRange range, Document doc, Element parent) {
149    Element node = doc.createElement("letter-grade-range");
150    node.setAttribute("letter-grade", range.letterGrade().asString());
151    node.setAttribute("minimum-score", String.valueOf(range.minimum()));
152    node.setAttribute("maximum-score", String.valueOf(range.maximum()));
153
154    parent.appendChild(node);
155  }
156
157  private static Document createDocumentForGradeBook(XmlHelper helper) {
158    Document doc = null;
159
160    try {
161      DocumentBuilderFactory factory =
162        DocumentBuilderFactory.newInstance();
163      factory.setValidating(true);
164
165      DocumentBuilder builder = factory.newDocumentBuilder();
166      builder.setErrorHandler(helper);
167      builder.setEntityResolver(helper);
168
169      DOMImplementation dom =
170        builder.getDOMImplementation();
171      DocumentType docType =
172        dom.createDocumentType("gradebook", publicID, systemID);
173      doc = dom.createDocument(null, "gradebook", docType);
174
175    } catch (ParserConfigurationException | DOMException ex) {
176      ex.printStackTrace(System.err);
177      System.exit(1);
178
179    }
180    return doc;
181  }
182
183  private static void appendXmlForStudents(GradeBook book, Document doc, Element root) {
184    // Students
185    Element studentsNode = doc.createElement("students");
186    for (String id : book.getStudentIds()) {
187      appendTextElementIfValueIsNotNull(studentsNode, "id", id);
188    }
189
190    root.appendChild(studentsNode);
191  }
192
193  private static void appendXmlForClassName(GradeBook book, Element root) {
194    appendTextElementIfValueIsNotNull(root, "name", book.getClassName());
195  }
196
197  private static void appendXmlForAssignments(GradeBook book, Document doc, Element root) {
198    // assignment nodes
199    Element assignments = doc.createElement("assignments");
200    for (String assignmentName : book.getAssignmentNames()) {
201      Assignment assign = book.getAssignment(assignmentName);
202      Element assignNode = doc.createElement("assignment");
203
204      setAssignmentTypeAttribute(assign, assignNode);
205      appendTextElementIfValueIsNotNull(assignNode, "name", assign.getName());
206      appendTextElementIfValueIsNotNull(assignNode, "description", assign.getDescription());
207      appendTextElementIfValueIsNotNull(assignNode, "points", String.valueOf(assign.getPoints()));
208      appendTextElementIfValueIsNotNull(assignNode, "canvas-id", String.valueOf(assign.getCanvasId()));
209      appendTextElementIfValueIsNotNull(assignNode, "due-date", assign.getDueDate());
210
211      doNotes(doc, assignNode, assign.getNotes());
212
213      assignments.appendChild(assignNode);
214
215    }
216    root.appendChild(assignments);
217  }
218
219  private static void appendTextElementIfValueIsNotNull(Element parent, String elementName, LocalDateTime dateTime) {
220    if (dateTime != null) {
221      appendTextElementIfValueIsNotNull(parent, elementName, dateTime.format(DATE_TIME_FORMAT));
222    }
223  }
224
225  private static void setAssignmentTypeAttribute(Assignment assignment, Element assignmentNode) {
226    Assignment.AssignmentType type = assignment.getType();
227    switch (type) {
228      case PROJECT:
229        assignmentNode.setAttribute("type", "PROJECT");
230        setProjectTypeAttribute(assignment, assignmentNode);
231        break;
232
233      case QUIZ:
234        assignmentNode.setAttribute("type", "QUIZ");
235        break;
236
237      case OTHER:
238        assignmentNode.setAttribute("type", "OTHER");
239        break;
240
241      case OPTIONAL:
242        assignmentNode.setAttribute("type", "OPTIONAL");
243        break;
244
245      case POA:
246        assignmentNode.setAttribute("type", "POA");
247        break;
248
249      default:
250        throw new IllegalArgumentException("Can't handle assignment " +
251          "type " + type);
252    }
253  }
254
255  private static void setProjectTypeAttribute(Assignment assignment, Element assignmentNode) {
256    ProjectType projectType = assignment.getProjectType();
257    if (projectType != null) {
258      switch (projectType) {
259        case APP_CLASSES:
260          assignmentNode.setAttribute("project-type", "APP_CLASSES");
261          break;
262
263        case TEXT_FILE:
264          assignmentNode.setAttribute("project-type", "TEXT_FILE");
265          break;
266
267        case PRETTY_PRINT:
268          assignmentNode.setAttribute("project-type", "PRETTY_PRINT");
269          break;
270
271        case KOANS:
272          assignmentNode.setAttribute("project-type", "KOANS");
273          break;
274
275        case XML:
276          assignmentNode.setAttribute("project-type", "XML");
277          break;
278
279        case REST:
280          assignmentNode.setAttribute("project-type", "REST");
281          break;
282
283        case ANDROID:
284          assignmentNode.setAttribute("project-type", "ANDROID");
285          break;
286
287        default:
288          throw new IllegalStateException("Can't handle project type: " + projectType);
289      }
290    }
291
292  }
293
294  private void dumpDirtyStudents(GradeBook book) {
295    book.forEachStudent(student -> {
296      if (student.isDirty()) {
297        dumpStudent(student);
298      }
299    });
300  }
301
302  /**
303   * Creates a <code>notes</code> XML element for a given
304   * <code>List</code> of notes.
305   */
306  private static void doNotes(Document doc, Element parent, List<String> notes) {
307    Element notesNode = doc.createElement("notes");
308    for (String note : notes) {
309      appendTextElementIfValueIsNotNull(notesNode, "note", note);
310    }
311
312    parent.appendChild(notesNode);
313  }
314
315  /**
316   * Dumps a <code>Student</code> out to an XML file whose name is
317   * based on the student's id and resides in the
318   * <code>studentDir</code>.
319   */
320  private void dumpStudent(Student student) {
321    Document doc = toXml(student);
322
323    // Now dump DOM tree to the file
324    File studentFile = new File(studentDir, student.getId() + ".xml");
325    PrintWriter pw = new PrintWriter(newFileWriter(studentFile), true);
326
327    try {
328      writeXmlToPrintWriter(doc, pw);
329
330    } catch (TransformerException ex) {
331      ex.printStackTrace(System.err);
332      System.exit(1);
333    }
334  }
335
336  private FileWriter newFileWriter(File studentFile) {
337    try {
338      return new FileWriter(studentFile);
339
340    } catch (IOException ex) {
341      throw new IllegalStateException("Couldn't create FileWriter for " + studentFile, ex);
342    }
343  }
344
345  /**
346   * Returns a DOM tree that represents a <code>Student</code>
347   */
348  public static Document toXml(Student student) {
349    Document doc = createXmlDocument();
350
351    Element root = doc.getDocumentElement();
352
353    appendStudentInformation(student, root);
354    appendGradesInformation(student, root);
355    appendLateInformation(student, root);
356    appendResubmittedInformation(student, root);
357    appendNotes(student, root);
358
359    return doc;
360  }
361
362  private static void appendNotes(Student student, Element parent) {
363    List<String> notes = student.getNotes();
364    if (!notes.isEmpty()) {
365      doNotes(parent.getOwnerDocument(), parent, notes);
366    }
367  }
368
369  private static void appendResubmittedInformation(Student student, Element parent) {
370    Document doc = parent.getOwnerDocument();
371    List<String> resubmitted = student.getResubmitted();
372    if (!resubmitted.isEmpty()) {
373      Element resubNode = doc.createElement("resubmitted");
374
375      for (String assignmentName : resubmitted) {
376        appendTextElementIfValueIsNotNull(resubNode, "name", assignmentName);
377      }
378
379      parent.appendChild(resubNode);
380    }
381  }
382
383  private static void appendLateInformation(Student student, Element parent) {
384    Document doc = parent.getOwnerDocument();
385    List<String> late = student.getLate();
386    if (!late.isEmpty()) {
387      Element lateNode = doc.createElement("late");
388
389      for (String assignmentName : late) {
390        appendTextElementIfValueIsNotNull(lateNode, "name", assignmentName);
391      }
392
393      parent.appendChild(lateNode);
394    }
395  }
396
397  private static void appendGradesInformation(Student student, Element parent) {
398    Document doc = parent.getOwnerDocument();
399    Iterator<String> gradeNames = student.getGradeNames().iterator();
400    if (gradeNames.hasNext()) {
401      Element gradesNode = doc.createElement("grades");
402      while (gradeNames.hasNext()) {
403        String gradeName = gradeNames.next();
404        Grade grade = student.getGrade(gradeName);
405
406        Element gradeNode = doc.createElement("grade");
407
408        appendTextElementIfValueIsNotNull(gradeNode, "name", grade.getAssignmentName());
409        appendTextElementIfValueIsNotNull(gradeNode, "score", String.valueOf(grade.getScore()));
410
411        appendSubmissionsInformation(grade.getSubmissionInfos(), gradeNode);
412
413        doNotes(doc, gradeNode, grade.getNotes());
414
415        if (grade.getScore() == Grade.INCOMPLETE) {
416          gradeNode.setAttribute("type", "INCOMPLETE");
417
418        } else if (grade.getScore() == Grade.NO_GRADE) {
419          gradeNode.setAttribute("type", "NO_GRADE");
420        }
421
422        gradesNode.appendChild(gradeNode);
423      }
424
425      parent.appendChild(gradesNode);
426    }
427  }
428
429  private static void appendSubmissionsInformation(List<Grade.SubmissionInfo> submissionInfos, Element parent) {
430    if (!submissionInfos.isEmpty()) {
431      Document doc = parent.getOwnerDocument();
432      Element submissions = doc.createElement("submissions");
433      parent.appendChild(submissions);
434
435      submissionInfos.forEach(info -> {
436        Element submissionInfo = doc.createElement("submission-info");
437        if (info.isLate()) {
438          submissionInfo.setAttribute("late", "true");
439        }
440        submissions.appendChild(submissionInfo);
441        appendSubmissionInformation(info, submissionInfo);
442      });
443    }
444
445  }
446
447  private static void appendSubmissionInformation(Grade.SubmissionInfo info, Element parent) {
448    appendTextElementIfValueIsNotNull(parent, "date", info.getSubmissionTime());
449    appendTextElementIfValueIsNotNull(parent, "estimated-hours", info.getEstimatedHours());
450  }
451
452  private static void appendStudentInformation(Student student, Element root) {
453    appendTextElementIfValueIsNotNull(root, "id", student.getId());
454    appendTextElementIfValueIsNotNull(root, "firstName", student.getFirstName());
455    appendTextElementIfValueIsNotNull(root, "lastName", student.getLastName());
456    appendTextElementIfValueIsNotNull(root, "nickName", student.getNickName());
457    appendTextElementIfValueIsNotNull(root, "email", student.getEmail());
458    appendTextElementIfValueIsNotNull(root, "major", student.getMajor());
459    appendTextElementIfValueIsNotNull(root, "canvas-id", student.getCanvasId());
460    appendTextElementIfValueIsNotNull(root, "letter-grade", Objects.toString(student.getLetterGrade(), null));
461
462    setAttributeIfValueIsNotNull(root, "enrolled-section", getSectionXmlAttributeValue(student.getEnrolledSection()));
463  }
464
465  private static void setAttributeIfValueIsNotNull(Element root, String name, String value) {
466    if (value != null) {
467      root.setAttribute(name, value);
468    }
469  }
470
471  private static Document createXmlDocument() {
472    Document doc = null;
473
474    try {
475      DocumentBuilderFactory factory =
476        DocumentBuilderFactory.newInstance();
477      factory.setValidating(true);
478
479      DocumentBuilder builder = factory.newDocumentBuilder();
480
481      DOMImplementation dom =
482        builder.getDOMImplementation();
483      DocumentType docType =
484        dom.createDocumentType("student", publicID, systemID);
485      doc = dom.createDocument(null, "student", docType);
486
487    } catch (ParserConfigurationException | DOMException ex) {
488      ex.printStackTrace(System.err);
489      System.exit(1);
490    }
491    return doc;
492  }
493
494  private static Element appendTextElementIfValueIsNotNull(Element parent, String elementName, Object textValue) {
495    if (textValue != null) {
496      Document doc = parent.getOwnerDocument();
497      Element id = doc.createElement(elementName);
498      id.appendChild(doc.createTextNode(String.valueOf(textValue)));
499      parent.appendChild(id);
500      return id;
501
502    } else {
503      return null;
504    }
505  }
506
507}