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