001package edu.pdx.cs.joy.grader.gradebook;
002
003import edu.pdx.cs.joy.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.cs.joy.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 DATABASE:
280          assignmentNode.setAttribute("project-type", "DATABASE");
281          break;
282
283        case REST:
284          assignmentNode.setAttribute("project-type", "REST");
285          break;
286
287        case ANDROID:
288          assignmentNode.setAttribute("project-type", "ANDROID");
289          break;
290
291        default:
292          throw new IllegalStateException("Can't handle project type: " + projectType);
293      }
294    }
295
296  }
297
298  private void dumpDirtyStudents(GradeBook book) {
299    book.forEachStudent(student -> {
300      if (student.isDirty()) {
301        dumpStudent(student);
302      }
303    });
304  }
305
306  /**
307   * Creates a <code>notes</code> XML element for a given
308   * <code>List</code> of notes.
309   */
310  private static void doNotes(Document doc, Element parent, List<String> notes) {
311    Element notesNode = doc.createElement("notes");
312    for (String note : notes) {
313      appendTextElementIfValueIsNotNull(notesNode, "note", note);
314    }
315
316    parent.appendChild(notesNode);
317  }
318
319  /**
320   * Dumps a <code>Student</code> out to an XML file whose name is
321   * based on the student's id and resides in the
322   * <code>studentDir</code>.
323   */
324  private void dumpStudent(Student student) {
325    Document doc = toXml(student);
326
327    // Now dump DOM tree to the file
328    File studentFile = new File(studentDir, student.getId() + ".xml");
329    PrintWriter pw = new PrintWriter(newFileWriter(studentFile), true);
330
331    try {
332      writeXmlToPrintWriter(doc, pw);
333
334    } catch (TransformerException ex) {
335      ex.printStackTrace(System.err);
336      System.exit(1);
337    }
338  }
339
340  private FileWriter newFileWriter(File studentFile) {
341    try {
342      return new FileWriter(studentFile);
343
344    } catch (IOException ex) {
345      throw new IllegalStateException("Couldn't create FileWriter for " + studentFile, ex);
346    }
347  }
348
349  /**
350   * Returns a DOM tree that represents a <code>Student</code>
351   */
352  public static Document toXml(Student student) {
353    Document doc = createXmlDocument();
354
355    Element root = doc.getDocumentElement();
356
357    appendStudentInformation(student, root);
358    appendGradesInformation(student, root);
359    appendLateInformation(student, root);
360    appendResubmittedInformation(student, root);
361    appendNotes(student, root);
362
363    return doc;
364  }
365
366  private static void appendNotes(Student student, Element parent) {
367    List<String> notes = student.getNotes();
368    if (!notes.isEmpty()) {
369      doNotes(parent.getOwnerDocument(), parent, notes);
370    }
371  }
372
373  private static void appendResubmittedInformation(Student student, Element parent) {
374    Document doc = parent.getOwnerDocument();
375    List<String> resubmitted = student.getResubmitted();
376    if (!resubmitted.isEmpty()) {
377      Element resubNode = doc.createElement("resubmitted");
378
379      for (String assignmentName : resubmitted) {
380        appendTextElementIfValueIsNotNull(resubNode, "name", assignmentName);
381      }
382
383      parent.appendChild(resubNode);
384    }
385  }
386
387  private static void appendLateInformation(Student student, Element parent) {
388    Document doc = parent.getOwnerDocument();
389    List<String> late = student.getLate();
390    if (!late.isEmpty()) {
391      Element lateNode = doc.createElement("late");
392
393      for (String assignmentName : late) {
394        appendTextElementIfValueIsNotNull(lateNode, "name", assignmentName);
395      }
396
397      parent.appendChild(lateNode);
398    }
399  }
400
401  private static void appendGradesInformation(Student student, Element parent) {
402    Document doc = parent.getOwnerDocument();
403    Iterator<String> gradeNames = student.getGradeNames().iterator();
404    if (gradeNames.hasNext()) {
405      Element gradesNode = doc.createElement("grades");
406      while (gradeNames.hasNext()) {
407        String gradeName = gradeNames.next();
408        Grade grade = student.getGrade(gradeName);
409
410        Element gradeNode = doc.createElement("grade");
411
412        appendTextElementIfValueIsNotNull(gradeNode, "name", grade.getAssignmentName());
413        appendTextElementIfValueIsNotNull(gradeNode, "score", String.valueOf(grade.getScore()));
414
415        appendSubmissionsInformation(grade.getSubmissionInfos(), gradeNode);
416
417        doNotes(doc, gradeNode, grade.getNotes());
418
419        if (grade.getScore() == Grade.INCOMPLETE) {
420          gradeNode.setAttribute("type", "INCOMPLETE");
421
422        } else if (grade.getScore() == Grade.NO_GRADE) {
423          gradeNode.setAttribute("type", "NO_GRADE");
424        }
425
426        gradesNode.appendChild(gradeNode);
427      }
428
429      parent.appendChild(gradesNode);
430    }
431  }
432
433  private static void appendSubmissionsInformation(List<Grade.SubmissionInfo> submissionInfos, Element parent) {
434    if (!submissionInfos.isEmpty()) {
435      Document doc = parent.getOwnerDocument();
436      Element submissions = doc.createElement("submissions");
437      parent.appendChild(submissions);
438
439      submissionInfos.forEach(info -> {
440        Element submissionInfo = doc.createElement("submission-info");
441        if (info.isLate()) {
442          submissionInfo.setAttribute("late", "true");
443        }
444        submissions.appendChild(submissionInfo);
445        appendSubmissionInformation(info, submissionInfo);
446      });
447    }
448
449  }
450
451  private static void appendSubmissionInformation(Grade.SubmissionInfo info, Element parent) {
452    appendTextElementIfValueIsNotNull(parent, "date", info.getSubmissionTime());
453    appendTextElementIfValueIsNotNull(parent, "estimated-hours", info.getEstimatedHours());
454  }
455
456  private static void appendStudentInformation(Student student, Element root) {
457    appendTextElementIfValueIsNotNull(root, "id", student.getId());
458    appendTextElementIfValueIsNotNull(root, "firstName", student.getFirstName());
459    appendTextElementIfValueIsNotNull(root, "lastName", student.getLastName());
460    appendTextElementIfValueIsNotNull(root, "nickName", student.getNickName());
461    appendTextElementIfValueIsNotNull(root, "email", student.getEmail());
462    appendTextElementIfValueIsNotNull(root, "major", student.getMajor());
463    appendTextElementIfValueIsNotNull(root, "canvas-id", student.getCanvasId());
464    appendTextElementIfValueIsNotNull(root, "github-user-name", String.valueOf(student.getGitHubUserName()));
465    appendTextElementIfValueIsNotNull(root, "letter-grade", Objects.toString(student.getLetterGrade(), null));
466
467    setAttributeIfValueIsNotNull(root, "enrolled-section", getSectionXmlAttributeValue(student.getEnrolledSection()));
468  }
469
470  private static void setAttributeIfValueIsNotNull(Element root, String name, String value) {
471    if (value != null) {
472      root.setAttribute(name, value);
473    }
474  }
475
476  private static Document createXmlDocument() {
477    Document doc = null;
478
479    try {
480      DocumentBuilderFactory factory =
481        DocumentBuilderFactory.newInstance();
482      factory.setValidating(true);
483
484      DocumentBuilder builder = factory.newDocumentBuilder();
485
486      DOMImplementation dom =
487        builder.getDOMImplementation();
488      DocumentType docType =
489        dom.createDocumentType("student", publicID, systemID);
490      doc = dom.createDocument(null, "student", docType);
491
492    } catch (ParserConfigurationException | DOMException ex) {
493      ex.printStackTrace(System.err);
494      System.exit(1);
495    }
496    return doc;
497  }
498
499  private static Element appendTextElementIfValueIsNotNull(Element parent, String elementName, Object textValue) {
500    if (textValue != null) {
501      Document doc = parent.getOwnerDocument();
502      Element id = doc.createElement(elementName);
503      id.appendChild(doc.createTextNode(String.valueOf(textValue)));
504      parent.appendChild(id);
505      return id;
506
507    } else {
508      return null;
509    }
510  }
511
512}