package edu.pdx.cs410J.grader;
import edu.pdx.cs410J.ParserException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import static edu.pdx.cs410J.grader.GradeBook.LetterGradeRanges.LetterGradeRange;
/**
* This class creates a <code>GradeBook</code> from the contents of an
* XML file.
*/
public class XmlGradeBookParser extends XmlHelper {
private GradeBook book; // The grade book we're creating
private InputStream in; // Input source of grade book
private File studentDir; // Where the student XML file live
/**
* Creates an <code>XmlGradeBookParser</code> that creates a
* <code>GradeBook</code> from a file of a given name.
*/
public XmlGradeBookParser(String fileName) throws IOException {
this(new File(fileName));
}
/**
* Creates an <code>XmlGradeBookParser</code> that creates a
* <code>GradeBook</code> from the contents of a <code>File</code>.
*/
public XmlGradeBookParser(File file) throws IOException {
this(new FileInputStream(file));
this.setStudentDir(file.getCanonicalFile().getParentFile());
}
/**
* Creates an <code>XmlGradeBookParser</code> that creates a
* <code>GradeBook</code> from the contents of an
* <code>InputStream</code>.
*/
XmlGradeBookParser(InputStream in) {
this.in = in;
}
/**
* Sets the directory in which the XML files for students are
* generated.
*/
public void setStudentDir(File dir) {
if (!dir.exists()) {
throw new IllegalArgumentException(dir + " does not exist");
}
if (!dir.isDirectory()) {
throw new IllegalArgumentException(dir + " is not a directory");
}
this.studentDir = dir;
}
/**
* Extracts an <code>Assignment</code> from an <code>Element</code>
*/
private static Assignment extractAssignmentFrom(Element element)
throws ParserException {
Assignment assign = null;
String name = null;
String description = null;
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
if (child.getTagName().equals("name")) {
name = extractTextFrom(child);
} else if (child.getTagName().equals("description")) {
description = extractTextFrom(child);
} else if (child.getTagName().equals("points")) {
String points = extractTextFrom(child);
try {
if (name == null) {
throw new ParserException("No name for assignment with " +
"points " + points);
}
assign = new Assignment(name, Double.parseDouble(points));
if (description != null) {
assign.setDescription(description);
}
} catch (NumberFormatException ex) {
throw new ParserException("Invalid points value: " +
points);
}
} else if (child.getTagName().equals("due-date")) {
String dueDate = extractTextFrom(child);
try {
assign.setDueDate(LocalDateTime.parse(dueDate, DATE_TIME_FORMAT));
} catch(DateTimeParseException ex) {
throw new ParserException("Invalidate LocalDateTime: " + dueDate, ex);
}
} else if (child.getTagName().equals("notes")) {
for (String note : extractNotesFrom(child)) {
assign.addNote(note);
}
}
}
if (assign == null ) {
throw new ParserException("No assignment found!");
}
setAssignmentTypeFromXml(element, assign);
return assign;
}
private static void setAssignmentTypeFromXml(Element assignmentElement, Assignment assign) throws ParserException {
String type = assignmentElement.getAttribute("type");
switch (type) {
case "PROJECT":
assign.setType(Assignment.AssignmentType.PROJECT);
break;
case "QUIZ":
assign.setType(Assignment.AssignmentType.QUIZ);
break;
case "OTHER":
assign.setType(Assignment.AssignmentType.OTHER);
break;
case "OPTIONAL":
assign.setType(Assignment.AssignmentType.OPTIONAL);
break;
case "POA":
assign.setType(Assignment.AssignmentType.POA);
break;
default:
throw new ParserException("Unknown assignment type: " + type);
}
}
/**
* Parses the source and from it creates a <code>GradeBook</code>.
*/
public GradeBook parse() throws ParserException {
Document doc = parseDocumentFromInputStream();
Element root = null;
if (doc != null) {
root = doc.getDocumentElement();
}
if (doc == null || root == null) {
throw new ParserException("Document parsing failed");
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
if (child.getTagName().equals("name")) {
this.book = new GradeBook(extractTextFrom(child));
this.book.setDirty(false);
} else if (this.book == null) {
throw new ParserException("name element is not first");
} else if (child.getTagName().equals("assignments")) {
extractAssignmentsFrom(child);
} else if (child.getTagName().equals("letter-grade-ranges")) {
extractLetterGradeRangesFrom(child);
} else if (child.getTagName().equals("students")) {
extractStudentsFrom(child);
} else if (child.getTagName().equals("lateDays")) {
// Fill in later, maybe.
}
}
if (this.book != null) {
// The book is initially clean
this.book.makeClean();
}
return this.book;
}
private void extractLetterGradeRangesFrom(Element parent) {
Student.Section section;
String attributeName = "for-section";
if (parent.hasAttribute(attributeName)) {
String value = parent.getAttribute(attributeName);
section = Student.Section.fromString(value);
} else {
section = Student.Section.UNDERGRADUATE;
}
NodeList ranges = parent.getChildNodes();
for (int j = 0; j < ranges.getLength(); j++) {
Node range = ranges.item(j);
if (!(range instanceof Element)) {
continue;
}
extractLetterGradeRangeFrom((Element) range, section);
}
}
private void extractLetterGradeRangeFrom(Element element, Student.Section section) {
String letterGradeString = element.getAttribute("letter-grade");
LetterGrade letterGrade = LetterGrade.fromString(letterGradeString);
LetterGradeRange range = this.book.getLetterGradeRanges(section).getRange(letterGrade);
range.setRange(toInt(element.getAttribute("minimum-score")), toInt(element.getAttribute("maximum-score")));
}
private int toInt(String value) {
return Integer.parseInt(value);
}
private void extractStudentsFrom(Element child) throws ParserException {
NodeList students = child.getChildNodes();
for (int j = 0; j < students.getLength(); j++) {
Node student = students.item(j);
if (!(student instanceof Element)) {
continue;
}
Element idElement = (Element) student;
if (idElement.getTagName().equals("id")) {
String id = extractTextFrom(idElement);
// Locate the XML file for the Student
File file =
new File(this.studentDir, id + ".xml");
if (!file.exists()) {
throw new IllegalArgumentException("No XML file for " +
id);
}
try {
XmlStudentParser sp = new XmlStudentParser(file);
Student stu = sp.parseStudent();
this.book.addStudent(stu);
} catch (Exception ex) {
String s = "While parsing " + file + ": " + ex;
throw new ParserException(s);
}
}
}
}
private void extractAssignmentsFrom(Element child) throws ParserException {
NodeList assignments = child.getChildNodes();
for (int j = 0; j < assignments.getLength(); j++) {
Node assignment = assignments.item(j);
if (assignment instanceof Element) {
Assignment assign =
extractAssignmentFrom((Element) assignment);
this.book.addAssignment(assign);
}
}
}
private Document parseDocumentFromInputStream() throws ParserException {
// Parse the source
Document doc;
// Create a DOM tree from the XML source
try {
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
factory.setValidating(true);
DocumentBuilder builder =
factory.newDocumentBuilder();
builder.setErrorHandler(this);
builder.setEntityResolver(this);
doc = builder.parse(new InputSource(this.in));
} catch (ParserConfigurationException | SAXException | IOException ex) {
throw new ParserException("While parsing XML source: " + ex);
}
return doc;
}
}