001package edu.pdx.cs410J.grader.gradebook;
002
003import edu.pdx.cs410J.ParserException;
004import org.w3c.dom.Document;
005import org.w3c.dom.Element;
006import org.w3c.dom.Node;
007import org.w3c.dom.NodeList;
008import org.xml.sax.InputSource;
009import org.xml.sax.SAXException;
010
011import javax.xml.parsers.DocumentBuilder;
012import javax.xml.parsers.DocumentBuilderFactory;
013import javax.xml.parsers.ParserConfigurationException;
014import java.io.*;
015import java.time.LocalDateTime;
016import java.time.format.DateTimeParseException;
017
018import static edu.pdx.cs410J.grader.gradebook.Assignment.ProjectType.*;
019import static edu.pdx.cs410J.grader.gradebook.GradeBook.LetterGradeRanges.LetterGradeRange;
020
021/**
022 * This class creates a <code>GradeBook</code> from the contents of an
023 * XML file.
024 */
025public class XmlGradeBookParser extends XmlHelper {
026  private GradeBook book;    // The grade book we're creating
027  private InputStream in;    // Input source of grade book
028  private File studentDir;   // Where the student XML file live
029
030  /**
031   * Creates an <code>XmlGradeBookParser</code> that creates a
032   * <code>GradeBook</code> from a file of a given name.
033   */
034  public XmlGradeBookParser(String fileName) throws IOException {
035    this(new File(fileName));
036  }
037
038  /**
039   * Creates an <code>XmlGradeBookParser</code> that creates a
040   * <code>GradeBook</code> from the contents of a <code>File</code>.
041   */
042  public XmlGradeBookParser(File file) throws IOException {
043    this(new FileInputStream(file));
044    this.setStudentDir(file.getCanonicalFile().getParentFile());
045  }
046
047  /**
048   * Creates an <code>XmlGradeBookParser</code> that creates a
049   * <code>GradeBook</code> from the contents of an
050   * <code>InputStream</code>.
051   */
052  XmlGradeBookParser(InputStream in) {
053    this.in = in;
054  }
055
056  /**
057   * Sets the directory in which the XML files for students are
058   * generated.
059   */
060  public void setStudentDir(File dir) {
061    if (!dir.exists()) {
062      throw new IllegalArgumentException(dir + " does not exist");
063    }
064
065    if (!dir.isDirectory()) {
066      throw new IllegalArgumentException(dir + " is not a directory");
067    }
068
069    this.studentDir = dir;
070  }
071
072  /**
073   * Extracts an <code>Assignment</code> from an <code>Element</code>
074   */
075  private static Assignment extractAssignmentFrom(Element element)
076    throws ParserException {
077    Assignment assign = null;
078
079    String name = null;
080    String description = null;
081
082    NodeList children = element.getChildNodes();
083    for (int i = 0; i < children.getLength(); i++) {
084      Node node = children.item(i);
085      if (!(node instanceof Element)) {
086        continue;
087      }
088
089      Element child = (Element) node;
090      if (child.getTagName().equals("name")) {
091        name = extractTextFrom(child);
092
093      } else if (child.getTagName().equals("description")) {
094        description = extractTextFrom(child);
095
096      } else if (child.getTagName().equals("points")) {
097        String points = extractTextFrom(child);
098        try {
099          if (name == null) {
100            throw new ParserException("No name for assignment with " +
101              "points " + points);
102          }
103          assign = new Assignment(name, Double.parseDouble(points));
104          if (description != null) {
105            assign.setDescription(description);
106          }
107
108        } catch (NumberFormatException ex) {
109          throw new ParserException("Invalid points value: " +
110            points);
111        }
112
113      } else if (child.getTagName().equals("canvas-id")) {
114        String canvasId = extractTextFrom(child);
115        try {
116          assign.setCanvasId(Integer.parseInt(canvasId));
117
118        } catch(NumberFormatException ex) {
119          throw new ParserException("Invalidate Canvas Id: " + canvasId, ex);
120        }
121
122      } else if (child.getTagName().equals("due-date")) {
123        String dueDate = extractTextFrom(child);
124        try {
125          assign.setDueDate(LocalDateTime.parse(dueDate, DATE_TIME_FORMAT));
126
127        } catch(DateTimeParseException ex) {
128          throw new ParserException("Invalidate LocalDateTime: " + dueDate, ex);
129        }
130
131      } else if (child.getTagName().equals("notes")) {
132        for (String note : extractNotesFrom(child)) {
133          assign.addNote(note);
134        }
135      }
136    }
137
138    if (assign == null ) {
139      throw new ParserException("No assignment found!");
140    }
141
142    setAssignmentTypeFromXml(element, assign);
143
144    return assign;
145  }
146
147  private static void setAssignmentTypeFromXml(Element assignmentElement, Assignment assignment) throws ParserException {
148    String type = assignmentElement.getAttribute("type");
149    switch (type) {
150      case "PROJECT":
151        assignment.setType(Assignment.AssignmentType.PROJECT);
152        setProjectTypeFromXml(assignmentElement, assignment);
153        break;
154      case "QUIZ":
155        assignment.setType(Assignment.AssignmentType.QUIZ);
156        break;
157      case "OTHER":
158        assignment.setType(Assignment.AssignmentType.OTHER);
159        break;
160      case "OPTIONAL":
161        assignment.setType(Assignment.AssignmentType.OPTIONAL);
162        break;
163      case "POA":
164        assignment.setType(Assignment.AssignmentType.POA);
165        break;
166      default:
167        throw new ParserException("Unknown assignment type: " + type);
168    }
169  }
170
171  private static void setProjectTypeFromXml(Element assignmentElement, Assignment assignment) throws ParserException {
172    String projectType = assignmentElement.getAttribute("project-type");
173    if (projectType != null && projectType.length() > 0) {
174      switch (projectType) {
175        case "APP_CLASSES":
176          assignment.setProjectType(APP_CLASSES);
177          break;
178
179        case "TEXT_FILE":
180          assignment.setProjectType(TEXT_FILE);
181          break;
182
183        case "PRETTY_PRINT":
184          assignment.setProjectType(PRETTY_PRINT);
185          break;
186
187        case "KOANS":
188          assignment.setProjectType(KOANS);
189          break;
190
191        case "XML":
192          assignment.setProjectType(XML);
193          break;
194
195        case "REST":
196          assignment.setProjectType(REST);
197          break;
198
199        case "ANDROID":
200          assignment.setProjectType(ANDROID);
201          break;
202
203        default:
204          throw new ParserException("Unknown project type: " + projectType);
205      }
206    }
207  }
208
209  /**
210   * Parses the source and from it creates a <code>GradeBook</code>.
211   */
212  public GradeBook parse() throws ParserException {
213    Document doc = parseDocumentFromInputStream();
214
215
216    Element root = null;
217    if (doc != null) {
218      root = doc.getDocumentElement();
219    }
220    if (doc == null || root == null) {
221      throw new ParserException("Document parsing failed");
222    }
223
224    NodeList children = root.getChildNodes();
225    for (int i = 0; i < children.getLength(); i++) {
226      Node node = children.item(i);
227      if (!(node instanceof Element)) {
228        continue;
229      }
230
231      Element child = (Element) node;
232      if (child.getTagName().equals("name")) {
233        this.book = new GradeBook(extractTextFrom(child));
234              this.book.setDirty(false);
235
236      } else if (this.book == null) {
237        throw new ParserException("name element is not first");
238
239      } else if (child.getTagName().equals("assignments")) {
240        extractAssignmentsFrom(child);
241
242      } else if (child.getTagName().equals("letter-grade-ranges")) {
243        extractLetterGradeRangesFrom(child);
244
245      } else if (child.getTagName().equals("section-name")) {
246        extractSectionName(child);
247
248      } else if (child.getTagName().equals("students")) {
249        extractStudentsFrom(child);
250
251      } else if (child.getTagName().equals("lateDays")) {
252        // Fill in later, maybe.
253      }
254    }
255
256    if (this.book != null) {
257      // The book is initially clean
258      this.book.makeClean();
259    }
260
261    return this.book;
262  }
263
264  private void extractSectionName(Element element) {
265    Student.Section section = extractSection(element);
266    String sectionName = extractTextFrom(element);
267    this.book.setSectionName(section, sectionName);
268  }
269
270  private void extractLetterGradeRangesFrom(Element parent) {
271    Student.Section section = extractSection(parent);
272
273    NodeList ranges = parent.getChildNodes();
274    for (int j = 0; j < ranges.getLength(); j++) {
275      Node range = ranges.item(j);
276
277      if (!(range instanceof Element)) {
278        continue;
279      }
280
281      extractLetterGradeRangeFrom((Element) range, section);
282    }
283  }
284
285  private Student.Section extractSection(Element parent) {
286    Student.Section section;
287    String attributeName = "for-section";
288    if (parent.hasAttribute(attributeName)) {
289      String value = parent.getAttribute(attributeName);
290      section = Student.Section.fromString(value);
291
292    } else {
293      section = Student.Section.UNDERGRADUATE;
294    }
295    return section;
296  }
297
298  private void extractLetterGradeRangeFrom(Element element, Student.Section section) {
299    String letterGradeString = element.getAttribute("letter-grade");
300    LetterGrade letterGrade = LetterGrade.fromString(letterGradeString);
301    LetterGradeRange range = this.book.getLetterGradeRanges(section).getRange(letterGrade);
302    range.setRange(toInt(element.getAttribute("minimum-score")), toInt(element.getAttribute("maximum-score")));
303
304  }
305
306  private int toInt(String value) {
307    return Integer.parseInt(value);
308  }
309
310  private void extractStudentsFrom(Element child) throws ParserException {
311    NodeList students = child.getChildNodes();
312    for (int j = 0; j < students.getLength(); j++) {
313      Node student = students.item(j);
314
315      if (!(student instanceof Element)) {
316        continue;
317      }
318
319      Element idElement = (Element) student;
320      if (idElement.getTagName().equals("id")) {
321        String id = extractTextFrom(idElement);
322            // Locate the XML file for the Student
323        File file =
324          new File(this.studentDir, id + ".xml");
325        if (!file.exists()) {
326          throw new IllegalArgumentException("No XML file for " +
327                                             id);
328        }
329
330        try {
331          XmlStudentParser sp = new XmlStudentParser(file);
332          Student stu = sp.parseStudent();
333          this.book.addStudent(stu);
334
335        } catch (Exception ex) {
336          String s = "While parsing " + file + ": " + ex;
337          throw new ParserException(s);
338        }
339      }
340    }
341  }
342
343  private void extractAssignmentsFrom(Element child) throws ParserException {
344    NodeList assignments = child.getChildNodes();
345    for (int j = 0; j < assignments.getLength(); j++) {
346      Node assignment = assignments.item(j);
347      if (assignment instanceof Element) {
348        Assignment assign =
349          extractAssignmentFrom((Element) assignment);
350        this.book.addAssignment(assign);
351      }
352    }
353  }
354
355  private Document parseDocumentFromInputStream() throws ParserException {
356    // Parse the source
357    Document doc;
358
359    // Create a DOM tree from the XML source
360    try {
361      DocumentBuilderFactory factory =
362        DocumentBuilderFactory.newInstance();
363      factory.setValidating(true);
364
365      DocumentBuilder builder =
366        factory.newDocumentBuilder();
367      builder.setErrorHandler(this);
368      builder.setEntityResolver(this);
369
370      doc = builder.parse(new InputSource(this.in));
371
372    } catch (ParserConfigurationException | SAXException | IOException ex) {
373      throw new ParserException("While parsing XML source: " + ex);
374
375    }
376    return doc;
377  }
378
379}