me.vertretungsplan.parser.IphisParser.java Source code

Java tutorial

Introduction

Here is the source code for me.vertretungsplan.parser.IphisParser.java

Source

/*
 * substitution-schedule-parser - Java library for parsing schools' substitution schedules
 * Copyright (c) 2016 Johan v. Forstner
 * Copyright (c) 2017 Tobias Knipping
 *
 * 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 io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import me.vertretungsplan.exception.CredentialInvalidException;
import me.vertretungsplan.objects.*;
import me.vertretungsplan.objects.credential.UserPasswordCredential;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.client.HttpResponseException;
import org.apache.http.entity.ContentType;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parser for substitution schedules from IPHIS.
 * <p>
 * More information can be found on the <a href="https://www.tk-schulsoftware.de">official website</a>.
 * <p>
 * This parser can be accessed using <code>"iphis"</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>url</code> (String, required)</dt>
 * <dd>The URL of the IPHIS Instance.</dd>
 *
 * <dt><code>kuerzel</code> (String, required)</dt>
 * <dd>The school shortcode required for IPHIS.</dd>
 *
 * <dt><code>jwt_key</code> (String, required)</dt>
 * <dd>The key used for signing the JWT</dd>
 * </dl>
 *
 * You have to use a {@link me.vertretungsplan.objects.authentication.UserPasswordAuthenticationData} because all
 * schedules on IPHIS are protected by a login.
 */
public class IphisParser extends BaseParser {

    private static final String PARAM_URL = "url";
    private static final String PARAM_JWT_KEY = "jwt_key";
    private static final String PARAM_KUERZEL = "kuerzel";

    /**
     * URL of given IPHIS instance
     */
    private String api;

    /**
     * Shortcode for school
     */
    private String kuerzel;
    /** */
    private String jwt_key;

    /**  */
    private String website;

    /**
     * array of grades/classes retrieved from the api
     */
    private JSONArray grades;
    /**
     * array of teachers retrieved from the api
     */
    private JSONArray teachers;
    /**
     * array of messages retrieved from the api
     */
    private JSONArray messages;
    /**
     * hold the Authentication Token (JWT)
     */
    private String authToken;
    /**
     * hold the timestamp of the last schedule-update
     */
    private LocalDateTime lastUpdate;

    public IphisParser(SubstitutionScheduleData scheduleData, CookieProvider cookieProvider) {
        super(scheduleData, cookieProvider);
        JSONObject data = scheduleData.getData();
        try {
            api = "https://" + data.getString(PARAM_URL) + "/remote/vertretungsplan/ssp";
            kuerzel = data.getString(PARAM_KUERZEL);
            jwt_key = data.getString(PARAM_JWT_KEY);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    public SubstitutionSchedule getSubstitutionSchedule()
            throws IOException, JSONException, CredentialInvalidException {
        final SubstitutionSchedule substitutionSchedule = SubstitutionSchedule.fromData(scheduleData);

        if (login()) {
            getGrades();
            getTeachers();
            getMessages();

            final JSONArray changes = getChanges();

            substitutionSchedule.setClasses(getAllClasses());
            substitutionSchedule.setTeachers(getAllTeachers());
            substitutionSchedule.setWebsite(website);

            parseIphis(substitutionSchedule, changes, grades, teachers, messages);
        }
        return substitutionSchedule;
    }

    @Override
    public LocalDateTime getLastChange() throws IOException, JSONException, CredentialInvalidException {
        if (lastUpdate == null) {
            login();
        }
        return lastUpdate;
    }

    private Boolean login() throws CredentialInvalidException, IOException {
        final UserPasswordCredential userPasswordCredential = (UserPasswordCredential) credential;
        final String username = userPasswordCredential.getUsername();
        final String password = userPasswordCredential.getPassword();

        JSONObject payload = new JSONObject();
        try {
            payload.put("school", kuerzel);
            payload.put("user", username);
            payload.put("type", scheduleData.getType());
            payload.put("password", password);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        httpPost(api + "/login", "UTF-8", payload.toString(), ContentType.APPLICATION_JSON);
        final String httpResponse = httpPost(api + "/login", "UTF-8", payload.toString(),
                ContentType.APPLICATION_JSON);
        final JSONObject token;
        try {
            token = new JSONObject(httpResponse);

            final String key = Base64.encodeBase64String(jwt_key.getBytes());
            final Claims jwtToken = Jwts.parser().setSigningKey(key).parseClaimsJws(token.getString("token"))
                    .getBody();
            assert jwtToken.getSubject().equals("vertretungsplan.me");

            authToken = token.getString("token");
            website = jwtToken.getIssuer();
            lastUpdate = new LocalDateTime(token.getLong("stand") * 1000);
        } catch (SignatureException | JSONException e) {
            throw new CredentialInvalidException();
        }

        return true;
    }

    /**
     * Returns a JSONArray with all changes from now to in one week.
     */
    private JSONArray getChanges() throws IOException, CredentialInvalidException {
        // Date (or alias of date) when the changes start
        final String startBy = LocalDate.now().toString();
        // Date (or alias of date) when the changes end
        final String endBy = LocalDate.now().plusWeeks(1).toString();

        final String url = api + "/vertretung/von/" + startBy + "/bis/" + endBy;
        return getJSONArray(url);
    }

    /**
     * Returns a JSONArray with all messages.
     */
    private void getMessages() throws IOException, JSONException, CredentialInvalidException {
        if (messages == null) {
            final String url = api + "/nachrichten";
            messages = getJSONArray(url);
        }
    }

    /**
     * Returns a JSONArray with all grades.
     */
    private void getGrades() throws IOException, JSONException, CredentialInvalidException {
        if (grades == null) {
            final String url = api + "/klassen";
            grades = getJSONArray(url);
        }
    }

    /**
     * Returns a JSONArray with all teachers.
     */
    private void getTeachers() throws IOException, CredentialInvalidException {
        if (teachers == null) {
            final String url = api + "/lehrer";
            teachers = getJSONArray(url);
        }
    }

    private JSONArray getJSONArray(String url) throws IOException, CredentialInvalidException {
        try {
            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "Bearer " + authToken);
            headers.put("Content-Type", "application/json");
            headers.put("Accept", "application/json");

            final String httpResponse = httpGet(url, "UTF-8", headers);
            return new JSONArray(httpResponse);
        } catch (HttpResponseException httpResponseException) {
            if (httpResponseException.getStatusCode() == 404) {
                return null;
            }
            throw httpResponseException;
        } catch (JSONException e) {
            return new JSONArray();
        }
    }

    void parseIphis(SubstitutionSchedule substitutionSchedule, JSONArray changes, JSONArray grades,
            JSONArray teachers, JSONArray messages) throws IOException, JSONException {
        if (changes == null) {
            return;
        }
        // Link course IDs to their names
        HashMap<String, String> coursesHashMap = null;
        if (grades != null) {
            coursesHashMap = new HashMap<>();
            for (int i = 0; i < grades.length(); i++) {
                JSONObject grade = grades.getJSONObject(i);
                coursesHashMap.put(grade.getString("id"), grade.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 Messages
        List<AdditionalInfo> infos = new ArrayList<>(messages.length());

        for (int i = 0; i < messages.length(); i++) {
            JSONObject message = messages.getJSONObject(i);
            AdditionalInfo info = new AdditionalInfo();
            info.setHasInformation(message.getBoolean("notification"));
            info.setTitle(message.getString("titel").trim());
            info.setText(message.getString("nachricht").trim());
            info.setFromSchedule(true);
            infos.add(info);
        }

        substitutionSchedule.getAdditionalInfos().addAll(infos);
        substitutionSchedule.setLastChange(lastUpdate);

        // 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 LocalDate substitutionDate = new LocalDate(change.getString("datum"));

            // If starting date of change does not equal date of SubstitutionScheduleDay
            if (!substitutionDate.isEqual(currentDate)) {
                if (!substitutionScheduleDay.getSubstitutions().isEmpty()
                        || !substitutionScheduleDay.getMessages().isEmpty()) {
                    substitutionSchedule.addDay(substitutionScheduleDay);
                }
                substitutionScheduleDay = new SubstitutionScheduleDay();
                substitutionScheduleDay.setDate(substitutionDate);
                currentDate = substitutionDate;
            }

            if (change.getInt("id") > 0) {
                final Substitution substitution = getSubstitution(change, coursesHashMap, teachersHashMap);

                substitutionScheduleDay.addSubstitution(substitution);
            } else if (!change.optString("nachricht").isEmpty()) {
                substitutionScheduleDay.addMessage(change.optString("nachricht"));
            }
        }
        substitutionSchedule.addDay(substitutionScheduleDay);
    }

    private String[] getSQLArray(String data) {
        String[] retArray = {};
        Pattern pattern = Pattern.compile("\\{(.*?)}");
        Matcher matcher = pattern.matcher(data);
        if (matcher.find()) {
            retArray = matcher.group(1).split(",");
        }
        return retArray;
    }

    private Substitution getSubstitution(JSONObject change, HashMap<String, String> gradesHashMap,
            HashMap<String, String> teachersHashMap) throws IOException, JSONException {
        final Substitution substitution = new Substitution();
        // Set class(es)
        final String[] classIds = getSQLArray(change.getString("id_klasse"));
        if (classIds.length > 0) {
            if (gradesHashMap == null) {
                throw new IOException("Change references a grade but grades are empty.");
            }
            final HashSet<String> classes = new HashSet<>();
            for (String classId : classIds) {
                if (!classId.toLowerCase().equals("null")) {
                    if (gradesHashMap.containsKey(classId)) {
                        classes.add(gradesHashMap.get(classId));
                    } else {
                        throw new IllegalArgumentException("unknown class ID " + classId + " referenced");
                    }
                }
            }
            substitution.setClasses(classes);
        }
        // Set type
        final String type = change.getString("aenderungsgrund").trim();
        if (!type.isEmpty() && !type.toLowerCase().equals("null")) {
            substitution.setType(type);
        } else {
            substitution.setType("Vertretung");
        }

        // Set color
        substitution.setColor(colorProvider.getColor(type));
        // Set covering teacher
        final String[] coveringTeacherIds = getSQLArray(change.getString("id_person_verantwortlich"));
        if (coveringTeacherIds.length > 0) {
            if (teachersHashMap == null) {
                throw new IOException("Change references a covering teacher but teachers are empty.");
            }
            final HashSet<String> teachers = new HashSet<>();
            for (String coveringTeacherId : coveringTeacherIds) {
                if (!coveringTeacherId.toLowerCase().equals("null")
                        && teachersHashMap.get(coveringTeacherId) != null) {
                    teachers.add(teachersHashMap.get(coveringTeacherId));
                }
            }
            substitution.setTeachers(teachers);
        }
        // Set teacher
        final String[] teacherIds = getSQLArray(change.getString("id_person_verantwortlich_orig"));
        final HashSet<String> coveringTeachers = new HashSet<>();
        if (teacherIds.length > 0) {
            if (teachersHashMap == null) {
                throw new IOException("Change references a teacher but teachers are empty.");
            }
            for (String coveringTeacherId : coveringTeacherIds) {
                if (!coveringTeacherId.toLowerCase().equals("null")
                        && teachersHashMap.get(coveringTeacherId) != null) {
                    coveringTeachers.add(teachersHashMap.get(coveringTeacherId));
                }
            }
            substitution.setPreviousTeachers(coveringTeachers);
        }

        //Set room
        if (!change.optString("raum").isEmpty() && !change.optString("raum").toLowerCase().equals("null")) {
            substitution.setRoom(change.optString("raum"));
        } else if (!change.optString("raum_orig").isEmpty()
                && !change.optString("raum_orig").toLowerCase().equals("null")) {
            substitution.setRoom(change.optString("raum_orig"));
        }
        if (!change.optString("raum_orig").isEmpty()
                && !change.optString("raum_orig").toLowerCase().equals("null")) {
            substitution.setPreviousRoom(change.optString("raum_orig"));
        } else if (!change.optString("raum").isEmpty() && !change.optString("raum").toLowerCase().equals("null")) {
            substitution.setPreviousRoom(change.optString("raum"));
        }
        //Set subject
        if (!change.optString("fach").isEmpty() && !change.optString("fach").toLowerCase().equals("null")) {
            substitution.setSubject(change.optString("fach"));
        }
        if (!change.optString("fach_orig").isEmpty()
                && !change.optString("fach_orig").toLowerCase().equals("null")) {
            substitution.setPreviousSubject(change.optString("fach_orig"));
        }

        //Set description
        if (!change.getString("information").isEmpty()
                && !change.getString("information").toLowerCase().equals("null")) {
            substitution.setDesc(change.getString("information").trim());
        }

        final String startingHour = change.getString("zeit_von").replaceFirst("^0+(?!$)", "");
        final String endingHour = change.getString("zeit_bis").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("")) {
                lesson = startingHour + " - " + endingHour;
            }
            if (startingHour.equals(endingHour)) {
                lesson = startingHour;
            }
            substitution.setLesson(lesson);
        }
        return substitution;
    }

    @Override
    public List<String> getAllClasses() throws IOException, JSONException, CredentialInvalidException {
        final List<String> classesList = new ArrayList<>();
        if (grades == null) {
            return null;
        }
        for (int i = 0; i < grades.length(); i++) {
            final JSONObject grade = grades.getJSONObject(i);
            classesList.add(grade.getString("name"));
        }
        return classesList;
    }

    @Override
    public List<String> getAllTeachers() throws IOException, JSONException, CredentialInvalidException {
        final List<String> teachersList = new ArrayList<>();
        if (teachers == null) {
            return null;
        }
        for (int i = 0; i < teachers.length(); i++) {
            final JSONObject teacher = teachers.getJSONObject(i);
            teachersList.add(teacher.getString("name"));
        }
        return teachersList;
    }
}