001package edu.pdx.cs410J.grader.gradebook;
002
003import edu.pdx.cs410J.ParserException;
004import edu.pdx.cs410J.grader.gradebook.Student.Section;
005
006import java.io.File;
007import java.io.FileNotFoundException;
008import java.io.IOException;
009import java.io.PrintWriter;
010import java.util.*;
011import java.util.function.Consumer;
012import java.util.stream.Stream;
013
014import static edu.pdx.cs410J.grader.gradebook.GradeBook.LetterGradeRanges.LetterGradeRange;
015
016/**
017 * This class represents a grade book that contains information about
018 * a CS410J class: the assignments, the students and their grades.
019 *
020 * @author David Whitlock
021 * @since Fall 2000
022 */
023public class GradeBook {
024
025  /** The name of the class */
026  private String className;
027
028  /** Maps the name of an assignment to an <code>Assignment</code> */
029  private Map<String, Assignment> assignments = new TreeMap<>();
030
031  /** Maps the id of a student to a <code>Student</code> object */
032  private Map<String, Student> students = new TreeMap<>();
033
034  /** Has the grade book been modified? */
035  private boolean dirty = true;
036
037  /** The Course Request Number (CRN) for this gradebook */
038  private int crn = 0;
039
040  private final Map<Section, LetterGradeRanges> letterGradeRanges = new HashMap<>();
041  private final Map<Section, String> sectionNames = new HashMap<>();
042
043  /**
044   * Creates a new <code>GradeBook</code> for a given class
045   */
046  public GradeBook(String className) {
047    this.className = className;
048    for(Section section : getSections()) {
049      letterGradeRanges.put(section, new LetterGradeRanges());
050    }
051  }
052
053  public Iterable<Section> getSections() {
054    return Arrays.asList(Section.UNDERGRADUATE, Section.GRADUATE);
055  }
056
057  /**
058   * Returns the name of the class represented by this
059   * <code>GradeBook</code>
060   */
061  public String getClassName() {
062    return this.className;
063  }
064
065  /**
066   * Returns the names of the assignments for this class
067   */
068  public Set<String> getAssignmentNames() {
069    return this.assignments.keySet();
070  }
071
072  /**
073   * Sets the Course Request Number (CRN) for this grade book.
074   *
075   * @since Spring 2005
076   */
077  public void setCRN(int crn) {
078    this.setDirty(true);
079    this.crn = crn;
080  }
081
082  /**
083   * Returns the Course Request Number (CRN) for this grade book.
084   *
085   * @since Spring 2005
086   */
087  public int getCRN() {
088    return this.crn;
089  }
090
091  /**
092   * Returns the <code>Assignment</code> of a given name
093   */
094  public Assignment getAssignment(String name) {
095    return this.assignments.get(name);
096  }
097
098  /**
099   * Adds an <code>Assignment</code> to this class
100   *
101   * @return <code>assignment</code> so that method calls can be chained
102   */
103  public Assignment addAssignment(Assignment assignment) {
104    this.setDirty(true);
105    this.assignments.put(assignment.getName(), assignment);
106    return assignment;
107  }
108
109  /**
110   * Returns the ids of all the students in this class
111   */
112  public Set<String> getStudentIds() {
113    return this.students.keySet();
114  }
115
116  public Optional<Student> getStudent(String id) {
117    return Optional.ofNullable(this.students.get(id));
118  }
119
120  public Stream<Student> studentsStream() {
121    return this.students.values().stream();
122  }
123
124  /**
125   * Adds a <code>Student</code> to this <code>GradeBook</code>
126   *
127   * @return <code>student</code> so that calls can be chained
128   * @see #containsStudent
129   */
130  public Student addStudent(Student student) {
131    this.setDirty(true);
132    this.students.put(student.getId(), student);
133    return student;
134  }
135
136  /**
137   * Removes a <code>Student</code> from this <code>GradeBook</code>
138   */
139  public void removeStudent(Student student) {
140    if (this.students.remove(student.getId()) != null) {
141      this.setDirty(true);
142    }
143  }
144
145  /**
146   * Returns whether or not this grade book contains a student with
147   * the given id.
148   */
149  public boolean containsStudent(String id) {
150    return this.students.containsKey(id);
151  }
152
153  /**
154   * Sets the dirtiness of this <code>GradeBook</code>
155   */
156  public void setDirty(boolean dirty) {
157    this.dirty = dirty;
158  }
159
160  /**
161   * Marks this <code>GradeBook</code> as being clean
162   */
163  public void makeClean() {
164    this.setDirty(false);
165
166    for (Assignment assignment : this.assignments.values()) {
167      assignment.makeClean();
168    }
169
170    for (Student student : this.students.values()) {
171      student.makeClean();
172    }
173  }
174
175  /**
176   * Returns <code>true</code> if this <code>GradeBook</code> has been
177   * modified.
178   */
179  public boolean isDirty() {
180    if (this.dirty) {
181      return true;
182    }
183
184    // Are any of the Assignments dirty?
185    Iterator iter = this.assignments.values().iterator();
186    while (iter.hasNext()) {
187      Assignment assign = (Assignment) iter.next();
188      if (assign.isDirty()) {
189        return true;
190      }
191    }
192
193    // Are any of the Students dirty?
194    iter = this.students.values().iterator();
195    while (iter.hasNext()) {
196      Student student = (Student) iter.next();
197      if (student.isDirty()) {
198        return true;
199      }
200    }
201
202    return false;
203  }
204
205  /**
206   * Returns a brief textual description of this
207   * <code>GradeBook</code>.
208   */
209  public String toString() {
210    return "Grade book for " + this.getClassName() + " with " +
211           this.getStudentIds().size() + " students";
212  }
213
214  private static PrintWriter err = new PrintWriter(System.err, true);
215
216  /**
217   * Prints usage information about the main program.
218   */
219  private static void usage() {
220    err.println("\nusage: java GradeBook -file xmlFile [options]");
221    err.println("Where [options] are:");
222    err.println("  -name className      Create new class file");
223    err.println("  -import xmlName      Import a student from a file");
224    err.println("\n");
225    System.exit(1);
226  }
227
228  /**
229   * Main program that is used to create a <code>GradeBook</code>
230   */
231  public static void main(String[] args) {
232    String xmlFile = null;
233    String name = null;
234    String importName = null;
235
236    // Parse the command line
237    for (int i = 0; i < args.length; i++) {
238      if (args[i].equals("-file")) {
239        if (++i >= args.length) {
240          err.println("** Missing file name");
241          usage();
242        }
243
244        xmlFile = args[i];
245
246      } else if (args[i].equals("-name")) {
247        if (++i >= args.length) {
248          err.println("** Missing class name");
249          usage();
250        }
251
252        name = args[i];
253
254      } else if (args[i].equals("-import")) {
255        if (++i >= args.length) {
256          err.println("** Missing import file name");
257          usage();
258        }
259
260        importName = args[i];
261
262      } else {
263        err.println("** Spurious command line option: " + args[i]);
264        usage();
265      }
266    }
267
268    if (xmlFile == null) {
269      err.println("** No XML file specified");
270      usage();
271    }
272
273    GradeBook book = null;
274
275    File file = new File(xmlFile);
276    if (file.exists()) {
277      // Parse a grade book from the XML file
278      try {
279        XmlGradeBookParser parser = new XmlGradeBookParser(file);
280        book = parser.parse();
281
282      } catch (FileNotFoundException ex) {
283        err.println("** Could not find file: " + ex.getMessage());
284        System.exit(1);
285
286      } catch (IOException ex) {
287        err.println("** IOException during parsing: " + ex.getMessage());
288        System.exit(1);
289
290      } catch (ParserException ex) {
291        err.println("** Error during parsing: " + ex.getMessage());
292        System.exit(1);
293      }
294
295      // Do we import?
296      if (importName != null) {
297        File importFile = new File(importName);
298        if (!importFile.exists()) {
299          err.println("** Import file " + importFile.getName() + 
300                      " does not exist");
301          System.exit(1);
302        }
303
304        try {
305          XmlStudentParser sp = new XmlStudentParser(importFile);
306          Student student = sp.parseStudent();
307          book.addStudent(student);
308
309        } catch (IOException ex) {
310          err.println("** Error during parsing: " + ex.getMessage());
311          System.exit(1);
312
313        } catch (ParserException ex) {
314          err.println("** Error during parsing: " + ex.getMessage());
315          System.exit(1);
316        }
317      }
318
319    } else if (name == null) {
320      err.println("** Must specify a class name when creating a " +
321                  "grade book");
322      System.exit(1);
323      
324    } else {
325      // Create an empty GradeBook
326      book = new GradeBook(name);
327    }
328
329    // Write the grade book to the XML file
330    try {
331      XmlDumper dumper = new XmlDumper(file);
332      dumper.dump(book);
333
334    } catch (IOException ex) {
335      err.println("** Error while writing XML file: " + ex);
336      System.exit(1);
337    }
338  }
339
340  public LetterGradeRanges getLetterGradeRanges(Section section) {
341    return letterGradeRanges.get(section);
342  }
343
344  public LetterGrade getLetterGradeForScore(Section section, double score) {
345    for (LetterGradeRange range : this.getLetterGradeRanges(section)) {
346      if (range.isScoreInRange(score)) {
347        return range.letterGrade();
348      }
349    }
350
351    throw new IllegalStateException("Could not find a letter grade range for " + score);
352  }
353
354  public void forEachStudent(Consumer<Student> consumer) {
355    this.students.values().forEach(consumer);
356  }
357
358  public Stream<Assignment> assignmentsStream() {
359    return this.assignments.values().stream();
360  }
361
362  public String getSectionName(Section section) {
363    return this.sectionNames.get(section);
364  }
365
366  public void setSectionName(Section section, String sectionName) {
367    this.sectionNames.put(section, sectionName);
368  }
369
370  public static class LetterGradeRanges implements Iterable<LetterGradeRanges.LetterGradeRange> {
371    private final Map<LetterGrade, LetterGradeRange> ranges = new TreeMap<>();
372
373    private LetterGradeRanges() {
374      createDefaultLetterGradeRange(LetterGrade.A, 94, 100);
375      createDefaultLetterGradeRange(LetterGrade.A_MINUS, 90, 93);
376      createDefaultLetterGradeRange(LetterGrade.B_PLUS, 87, 89);
377      createDefaultLetterGradeRange(LetterGrade.B, 83, 86);
378      createDefaultLetterGradeRange(LetterGrade.B_MINUS, 80, 82);
379      createDefaultLetterGradeRange(LetterGrade.C_PLUS, 77, 79);
380      createDefaultLetterGradeRange(LetterGrade.C, 73, 76);
381      createDefaultLetterGradeRange(LetterGrade.C_MINUS, 70, 72);
382      createDefaultLetterGradeRange(LetterGrade.D_PLUS, 67, 69);
383      createDefaultLetterGradeRange(LetterGrade.D, 63, 66);
384      createDefaultLetterGradeRange(LetterGrade.D_MINUS, 60, 62);
385      createDefaultLetterGradeRange(LetterGrade.F, 0, 59);
386    }
387
388    private void createDefaultLetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) {
389      this.ranges.put(letterGrade, new LetterGradeRange(letterGrade, minimum, maximum));
390    }
391
392    public LetterGradeRange getRange(LetterGrade letterGrade) {
393      return ranges.get(letterGrade);
394    }
395
396    public void validate() {
397      validateThatFRangeHasAMinimumOf0();
398      validateThatARangeContains100();
399      validateThatRangesAreContiguous();
400    }
401
402    private void validateThatRangesAreContiguous() {
403      LetterGradeRange previous = null;
404
405      for (LetterGrade letterGrade : ranges.keySet()) {
406        LetterGradeRange current = ranges.get(letterGrade);
407        if (previous != null) {
408          if (previous.minimum() != current.maximum() + 1) {
409            String s = "There is a gap between the range for " + previous + " and " + current;
410            throw new LetterGradeRange.InvalidLetterGradeRange(s);
411          }
412        }
413
414        previous = current;
415      }
416
417    }
418
419    private void validateThatARangeContains100() {
420      LetterGradeRange range = getRange(LetterGrade.A);
421      if (range.maximum() < 100) {
422        throw new LetterGradeRange.InvalidLetterGradeRange("The A range must contain 100");
423      }
424    }
425
426    private void validateThatFRangeHasAMinimumOf0() {
427      LetterGradeRange range = getRange(LetterGrade.F);
428      if (range.minimum() > 0) {
429        throw new LetterGradeRange.InvalidLetterGradeRange("The F range must contain zero");
430      }
431    }
432
433    @Override
434    public Iterator<LetterGradeRange> iterator() {
435      return this.ranges.values().iterator();
436    }
437
438    @Override
439    public void forEach(Consumer<? super LetterGradeRange> action) {
440      this.ranges.values().forEach(action);
441    }
442
443    @Override
444    public Spliterator<LetterGradeRange> spliterator() {
445      return this.ranges.values().spliterator();
446    }
447
448    public static class LetterGradeRange {
449      private final LetterGrade letterGrade;
450      private int maximum;
451      private int minimum;
452
453      public LetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) {
454        this.letterGrade = letterGrade;
455        setRange(minimum, maximum);
456      }
457
458      public void setRange(int minimum, int maximum) {
459        if (minimum >= maximum) {
460          String s = String.format("Minimum value (%d) must be less than maximum value (%d)",
461            minimum, maximum);
462          throw new InvalidLetterGradeRange(s);
463        }
464
465        this.minimum = minimum;
466        this.maximum = maximum;
467      }
468
469      public int minimum() {
470        return this.minimum;
471      }
472
473      public int maximum() {
474        return this.maximum;
475      }
476
477      @Override
478      public String toString() {
479        return "Range for " + letterGrade() + " is " + minimum() + " to " + maximum();
480      }
481
482      public LetterGrade letterGrade() {
483        return this.letterGrade;
484      }
485
486      public boolean isScoreInRange(double score) {
487        int intScore = (int) Math.round(score);
488        return intScore >= minimum() && intScore <= maximum();
489      }
490
491      public static class InvalidLetterGradeRange extends RuntimeException {
492        public InvalidLetterGradeRange(String message) {
493          super(message);
494        }
495      }
496    }
497  }
498}