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}