Java tutorial
/* * substitution-schedule-parser - Java library for parsing schools' substitution schedules * Copyright (c) 2016 Johan v. Forstner * Copyright (c) 2016 Nico Alt * * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package me.vertretungsplan.parser; import me.vertretungsplan.exception.CredentialInvalidException; import me.vertretungsplan.objects.Substitution; import me.vertretungsplan.objects.SubstitutionSchedule; import me.vertretungsplan.objects.SubstitutionScheduleData; import me.vertretungsplan.objects.SubstitutionScheduleDay; import me.vertretungsplan.objects.credential.Credential; import me.vertretungsplan.objects.credential.UserPasswordCredential; import org.apache.commons.codec.digest.DigestUtils; import org.apache.http.client.HttpResponseException; import org.joda.time.LocalDate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.*; /** * Parser for LegionBoard, an open source changes management system for schools. * <p> * More information can be found on the <a href="http://legionboard.org">official website</a> and on its * <a href="https://gitlab.com/groups/legionboard">project page on GitLab</a>. * <p> * This parser can be accessed using <code>"legionboard"</code> for {@link SubstitutionScheduleData#setApi(String)}. * * <h4>Configuration parameters</h4> * These parameters can be supplied in {@link SubstitutionScheduleData#setData(JSONObject)} to configure the parser: * * <dl> * <dt><code>api</code> (String, required)</dt> * <dd>The URL where the LegionBoard Heart API can be found.</dd> * * <dt><code>website</code> (String, recommended)</dt> * <dd>The URL of a website where the substitution schedule can be seen online. Normally, this would be the URL of the * LegionBoard Eye instance.</dd> * </dl> * * You have to use a {@link me.vertretungsplan.objects.authentication.UserPasswordAuthenticationData} because all * schedules on LegionBoard are protected by a login. */ public class LegionBoardParser extends BaseParser { private static final String PARAM_API = "api"; private static final String PARAM_WEBSITE = "website"; /** * URL of given LegionBoard Heart instance */ private String api; /** * URL of given LegionBoard Eye instance */ private String website; public LegionBoardParser(SubstitutionScheduleData scheduleData, CookieProvider cookieProvider) { super(scheduleData, cookieProvider); JSONObject data = scheduleData.getData(); try { api = data.getString(PARAM_API); website = data.getString(PARAM_WEBSITE); } catch (JSONException e) { e.printStackTrace(); } } public SubstitutionSchedule getSubstitutionSchedule() throws IOException, JSONException, CredentialInvalidException { final SubstitutionSchedule substitutionSchedule = SubstitutionSchedule.fromData(scheduleData); substitutionSchedule.setClasses(getAllClasses()); substitutionSchedule.setTeachers(getAllTeachers()); substitutionSchedule.setWebsite(website); final JSONArray changes = getChanges(); final JSONArray courses = getCourses(); final JSONArray teachers = getTeachers(); parseLegionBoard(substitutionSchedule, changes, courses, teachers); return substitutionSchedule; } /** * Returns authentication key as shown * <a href="https://gitlab.com/legionboard/heart/blob/master/doc/README.md">in the documentation</a>. */ private String getAuthenticationKey(Credential credential) { final UserPasswordCredential userPasswordCredential = (UserPasswordCredential) credential; final String username = userPasswordCredential.getUsername(); final String password = userPasswordCredential.getPassword(); return DigestUtils.sha256Hex(username.toLowerCase() + "//" + password); } /** * Returns a JSONArray with all changes from now to in one week. * More information: <a href="https://gitlab.com/legionboard/heart/blob/master/doc/changes/list.md">List changes</a> */ private JSONArray getChanges() throws IOException, JSONException, CredentialInvalidException { // Date (or alias of date) when the changes start final String startBy = "now"; // Date (or alias of date) when the changes end final String endBy = "i1w"; final String url = api + "/changes?startBy=" + startBy + "&endBy=" + endBy + "&k=" + getAuthenticationKey(getCredential()); return getJSONArray(url); } /** * Returns a JSONArray with all courses. * More information: <a href="https://gitlab.com/legionboard/heart/blob/master/doc/courses/list.md">List courses</a> */ private JSONArray getCourses() throws IOException, JSONException, CredentialInvalidException { final String url = api + "/courses?k=" + getAuthenticationKey(getCredential()); return getJSONArray(url); } /** * Returns a JSONArray with all teachers. * More information: <a href="https://gitlab.com/legionboard/heart/blob/master/doc/teachers/list.md">List teachers</a> */ private JSONArray getTeachers() throws IOException, JSONException, CredentialInvalidException { final String url = api + "/teachers?k=" + getAuthenticationKey(getCredential()); return getJSONArray(url); } private JSONArray getJSONArray(String url) throws IOException, JSONException, CredentialInvalidException { try { return new JSONArray(httpGet(url, "UTF-8")); } catch (HttpResponseException httpResponseException) { if (httpResponseException.getStatusCode() == 404) { return null; } throw httpResponseException; } } void parseLegionBoard(SubstitutionSchedule substitutionSchedule, JSONArray changes, JSONArray courses, JSONArray teachers) throws IOException, JSONException { if (changes == null) { return; } // Link course IDs to their names HashMap<String, String> coursesHashMap = null; if (courses != null) { coursesHashMap = new HashMap<>(); for (int i = 0; i < courses.length(); i++) { JSONObject course = courses.getJSONObject(i); coursesHashMap.put(course.getString("id"), course.getString("name")); } } // Link teacher IDs to their names HashMap<String, String> teachersHashMap = null; if (teachers != null) { teachersHashMap = new HashMap<>(); for (int i = 0; i < teachers.length(); i++) { JSONObject teacher = teachers.getJSONObject(i); teachersHashMap.put(teacher.getString("id"), teacher.getString("name")); } } // Add changes to SubstitutionSchedule LocalDate currentDate = LocalDate.now(); SubstitutionScheduleDay substitutionScheduleDay = new SubstitutionScheduleDay(); substitutionScheduleDay.setDate(currentDate); for (int i = 0; i < changes.length(); i++) { final JSONObject change = changes.getJSONObject(i); final Substitution substitution = getSubstitution(change, coursesHashMap, teachersHashMap); final LocalDate startingDate = new LocalDate(change.getString("startingDate")); final LocalDate endingDate = new LocalDate(change.getString("endingDate")); // Handle multi-day changes if (!startingDate.isEqual(endingDate)) { if (!substitutionScheduleDay.getSubstitutions().isEmpty()) { substitutionSchedule.addDay(substitutionScheduleDay); } for (int k = 0; k < 8; k++) { final LocalDate date = LocalDate.now().plusDays(k); if ((date.isAfter(startingDate) || date.isEqual(startingDate)) && (date.isBefore(endingDate) || date.isEqual(endingDate))) { substitutionScheduleDay = new SubstitutionScheduleDay(); substitutionScheduleDay.setDate(date); substitutionScheduleDay.addSubstitution(substitution); substitutionSchedule.addDay(substitutionScheduleDay); currentDate = date; } } continue; } // If starting date of change does not equal date of SubstitutionScheduleDay if (!startingDate.isEqual(currentDate)) { if (!substitutionScheduleDay.getSubstitutions().isEmpty()) { substitutionSchedule.addDay(substitutionScheduleDay); } substitutionScheduleDay = new SubstitutionScheduleDay(); substitutionScheduleDay.setDate(startingDate); currentDate = startingDate; } substitutionScheduleDay.addSubstitution(substitution); } substitutionSchedule.addDay(substitutionScheduleDay); } private Substitution getSubstitution(JSONObject change, HashMap<String, String> coursesHashMap, HashMap<String, String> teachersHashMap) throws IOException, JSONException { final Substitution substitution = new Substitution(); // Set class final String classId = change.getString("course"); if (!classId.equals("0")) { if (coursesHashMap == null) { throw new IOException("Change references a course but courses are empty."); } final String singleClass = coursesHashMap.get(classId); final HashSet<String> classes = new HashSet<>(); classes.add(singleClass); substitution.setClasses(classes); } // Set type String type = "Unknown"; switch (change.getString("type")) { case "0": type = "Entfall"; break; case "1": type = "Vertretung"; break; case "2": type = "Information"; break; } substitution.setType(type); // Set color substitution.setColor(colorProvider.getColor(type)); // Set covering teacher final String coveringTeacherId = change.getString("coveringTeacher"); if (!coveringTeacherId.equals("0")) { if (teachersHashMap == null) { throw new IOException("Change references a covering teacher but teachers are empty."); } if (!teachersHashMap.get(coveringTeacherId).equals("-")) { substitution.setTeacher(teachersHashMap.get(coveringTeacherId)); } } // Set teacher final String teacherId = change.getString("teacher"); if (!teacherId.equals("0")) { if (teachersHashMap == null) { throw new IOException("Change references a teacher but teachers are empty."); } if (!teachersHashMap.get(teacherId).equals("-")) { if (type.equals("Vertretung") || substitution.getTeacher() != null) { substitution.setPreviousTeacher(teachersHashMap.get(teacherId)); } else { substitution.setTeacher(teachersHashMap.get(teacherId)); } } } // Set description substitution.setDesc(change.getString("text")); // Set lesson final String startingHour = change.getString("startingHour").replaceFirst("^0+(?!$)", ""); final String endingHour = change.getString("endingHour").replaceFirst("^0+(?!$)", ""); if (!startingHour.equals("") || !endingHour.equals("")) { String lesson = ""; if (!startingHour.equals("") && endingHour.equals("")) { lesson = "Ab " + startingHour; } if (startingHour.equals("") && !endingHour.equals("")) { lesson = "Bis " + endingHour; } if (!startingHour.equals("") && !endingHour.equals("")) { if (startingHour.equals(endingHour)) { lesson = startingHour; } else { lesson = startingHour + " - " + endingHour; } } substitution.setLesson(lesson); } return substitution; } @Override public List<String> getAllClasses() throws IOException, JSONException, CredentialInvalidException { final List<String> classes = new ArrayList<>(); final JSONArray courses = getCourses(); if (courses == null) { return null; } for (int i = 0; i < courses.length(); i++) { final JSONObject course = courses.getJSONObject(i); if (!course.getBoolean("archived")) { classes.add(course.getString("name")); } } Collections.sort(classes); return classes; } @Override public List<String> getAllTeachers() throws IOException, JSONException, CredentialInvalidException { final List<String> teachers = new ArrayList<>(); final JSONArray jsonTeachers = getTeachers(); if (jsonTeachers == null) { return null; } for (int i = 0; i < jsonTeachers.length(); i++) { final JSONObject teacher = jsonTeachers.getJSONObject(i); if (!teacher.getBoolean("archived")) { teachers.add(teacher.getString("name")); } } Collections.sort(teachers); return teachers; } }