me.vertretungsplan.parser.BaseParser.java Source code

Java tutorial

Introduction

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

Source

/*
 * substitution-schedule-parser - Java library for parsing schools' substitution schedules
 * Copyright (c) 2016 Johan v. Forstner
 *
 * 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.networking.MultiTrustManager;
import me.vertretungsplan.objects.SubstitutionSchedule;
import me.vertretungsplan.objects.SubstitutionScheduleData;
import me.vertretungsplan.objects.credential.Credential;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.universalchardet.UniversalDetector;

import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Base class for {@link SubstitutionScheduleParser} implementations.
 */
public abstract class BaseParser implements SubstitutionScheduleParser {
    public static final String PARAM_CLASS_REGEX = "classRegex";
    private static final String PARAM_SSL_HOSTNAME = "sslHostname";
    protected SubstitutionScheduleData scheduleData;
    protected Executor executor;
    protected Credential credential;
    protected CookieStore cookieStore;
    protected ColorProvider colorProvider;
    protected CookieProvider cookieProvider;
    protected UniversalDetector encodingDetector;

    BaseParser(SubstitutionScheduleData scheduleData, CookieProvider cookieProvider) {
        this.scheduleData = scheduleData;
        this.cookieProvider = cookieProvider;
        this.cookieStore = new BasicCookieStore();
        this.colorProvider = new ColorProvider(scheduleData);
        this.encodingDetector = new UniversalDetector(null);

        try {
            KeyStore ks = loadKeyStore();
            MultiTrustManager multiTrustManager = new MultiTrustManager();
            multiTrustManager.addTrustManager(getDefaultTrustManager());
            multiTrustManager.addTrustManager(trustManagerFromKeystore(ks));

            TrustManager[] trustManagers = new TrustManager[] { multiTrustManager };
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustManagers, null);
            final HostnameVerifier hostnameVerifier;

            if (scheduleData.getData() != null && scheduleData.getData().has(PARAM_SSL_HOSTNAME)) {
                hostnameVerifier = new CustomHostnameVerifier(scheduleData.getData().getString(PARAM_SSL_HOSTNAME));
            } else {
                hostnameVerifier = new DefaultHostnameVerifier();
            }
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
                    new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }, null, hostnameVerifier);

            CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf)
                    .setRedirectStrategy(new LaxRedirectStrategy())
                    .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())
                    .build();
            this.executor = Executor.newInstance(httpclient).use(cookieStore);
        } catch (GeneralSecurityException | JSONException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Create an appropriate parser for a given school. Automatically uses the appropriate subclass depending on
     * {@link SubstitutionScheduleData#getApi()}.
     *
     * @param data a {@link SubstitutionScheduleData} object containing information about the substitution schedule
     * @return a {@link BaseParser} subclass able to parse the given schedule.
     */
    public static BaseParser getInstance(SubstitutionScheduleData data, @Nullable CookieProvider cookieProvider) {
        BaseParser parser = null;
        switch (data.getApi()) {
        case "untis-monitor":
            parser = new UntisMonitorParser(data, cookieProvider);
            break;
        case "untis-info":
            parser = new UntisInfoParser(data, cookieProvider);
            break;
        case "untis-info-headless":
            parser = new UntisInfoHeadlessParser(data, cookieProvider);
            break;
        case "untis-subst":
            parser = new UntisSubstitutionParser(data, cookieProvider);
            break;
        case "dsbmobile":
            parser = new DSBMobileParser(data, cookieProvider);
            break;
        case "dsblight":
            parser = new DSBLightParser(data, cookieProvider);
            break;
        case "svplan":
            parser = new SVPlanParser(data, cookieProvider);
            break;
        case "davinci":
            parser = new DaVinciParser(data, cookieProvider);
            break;
        case "eschool":
            parser = new ESchoolParser(data, cookieProvider);
            break;
        case "turbovertretung":
            parser = new TurboVertretungParser(data, cookieProvider);
            break;
        case "csv":
            parser = new CSVParser(data, cookieProvider);
            break;
        case "legionboard":
            parser = new LegionBoardParser(data, cookieProvider);
            break;
        case "indiware":
            parser = new IndiwareParser(data, cookieProvider);
            break;
        case "stundenplan24":
            parser = new IndiwareStundenplan24Parser(data, cookieProvider);
            break;
        case "webuntis":
            parser = new WebUntisParser(data, cookieProvider);
            break;
        }
        return parser;
    }

    private static X509TrustManager getDefaultTrustManager() throws GeneralSecurityException {
        return trustManagerFromKeystore(null);
    }

    private static X509TrustManager trustManagerFromKeystore(final KeyStore keystore)
            throws GeneralSecurityException {
        final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX", "SunJSSE");
        trustManagerFactory.init(keystore);

        final TrustManager[] tms = trustManagerFactory.getTrustManagers();

        for (final TrustManager tm : tms) {
            if (tm instanceof X509TrustManager) {
                return X509TrustManager.class.cast(tm);
            }
        }
        throw new IllegalStateException("Could not locate X509TrustManager!");
    }

    protected static String recognizeType(String text) {
        if (text.toLowerCase().contains("f.a.") || text.toLowerCase().contains("fllt aus")
                || text.toLowerCase().contains("faellt aus") || text.toLowerCase().contains("entfllt")) {
            return "Entfall";
        } else if (equalsOneOf(text, "Raumnderung", "Klasse frei", "Unterrichtstausch", "Freistunde",
                "Raumverlegung", "Selbstlernen", "Zusammenlegung", "HA", "Raum beachten", "Stundentausch",
                "Klausur", "Raum-Vertr.", "Betreuung", "Frei/Veranstaltung")) {
            return text;
        } else if (text.contains("verschoben")) {
            return "Verlegung";
        } else if (text.contains("genderter Raum")) {
            return "Raumnderung";
        } else if (text.contains("frei")) {
            return "Entfall";
        } else if (text.contains("Aufgaben")) {
            return "Aufgaben";
        } else {
            return null;
        }
    }

    private static boolean equalsOneOf(String container, String... strings) {
        for (String string : strings) {
            if (container.equals(string))
                return true;
        }
        return false;
    }

    public abstract SubstitutionSchedule getSubstitutionSchedule()
            throws IOException, JSONException, CredentialInvalidException;

    /**
     * Get a list of all available classes.
     *
     * @return a list of all available classes (also those not currently affected by the substitution schedule)
     * @throws IOException Connection or parsing error
     * @throws JSONException Error with the JSON configuration
     */
    public abstract List<String> getAllClasses() throws IOException, JSONException, CredentialInvalidException;

    /**
     * Get a list of all available teachers. Can also be <code>null</code>.
     *
     * @return a list of all available teachers (also those not currently affected by the substitution schedule)
     * @throws IOException   Connection or parsing error
     * @throws JSONException Error with the JSON configuration
     */
    @SuppressWarnings("SameReturnValue")
    public abstract List<String> getAllTeachers() throws IOException, JSONException, CredentialInvalidException;

    public Credential getCredential() {
        return credential;
    }

    public void setCredential(Credential credential) {
        if (!scheduleData.getAuthenticationData().getCredentialType().equals(credential.getClass())) {
            throw new IllegalArgumentException("Wrong credential type");
        }
        this.credential = credential;
    }

    protected String httpGet(String url, String encoding) throws IOException, CredentialInvalidException {
        return httpGet(url, encoding, null);
    }

    protected String httpGet(String url, String encoding, Map<String, String> headers)
            throws IOException, CredentialInvalidException {
        Request request = Request.Get(url).connectTimeout(15000).socketTimeout(15000);
        if (headers != null) {
            for (Entry<String, String> entry : headers.entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        }
        return executeRequest(encoding, request);
    }

    @Nullable
    private String executeRequest(String encoding, Request request) throws IOException, CredentialInvalidException {
        try {
            byte[] bytes = executor.execute(request).returnContent().asBytes();
            encoding = getEncoding(encoding, bytes);
            return new String(bytes, encoding);
        } catch (HttpResponseException e) {
            handleHttpResponseException(e);
            return null;
        } finally {
            encodingDetector.reset();
        }
    }

    @NotNull
    private String getEncoding(String defaultEncoding, byte[] bytes) {
        encodingDetector.handleData(bytes, 0, bytes.length);
        encodingDetector.dataEnd();
        String encoding = encodingDetector.getDetectedCharset();
        if (encoding == null || encoding.equals("GB18030"))
            encoding = defaultEncoding;
        if (encoding == null)
            encoding = "UTF-8";
        encodingDetector.reset();
        return encoding;
    }

    @SuppressWarnings("SameParameterValue")
    protected String httpPost(String url, String encoding, List<NameValuePair> formParams)
            throws IOException, CredentialInvalidException {
        return httpPost(url, encoding, formParams, null);
    }

    protected String httpPost(String url, String encoding, List<NameValuePair> formParams,
            Map<String, String> headers) throws IOException, CredentialInvalidException {
        Request request = Request.Post(url).bodyForm(formParams).connectTimeout(15000).socketTimeout(15000);
        if (headers != null) {
            for (Entry<String, String> entry : headers.entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        }
        return executeRequest(encoding, request);
    }

    @SuppressWarnings("SameParameterValue")
    protected String httpPost(String url, String encoding, String body, ContentType contentType)
            throws IOException, CredentialInvalidException {
        return httpPost(url, encoding, body, contentType, null);
    }

    @SuppressWarnings("SameParameterValue")
    protected String httpPost(String url, String encoding, String body, ContentType contentType,
            Map<String, String> headers) throws IOException, CredentialInvalidException {
        Request request = Request.Post(url).bodyString(body, contentType).connectTimeout(15000)
                .socketTimeout(15000);
        if (headers != null) {
            for (Entry<String, String> entry : headers.entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        }
        return executeRequest(encoding, request);
    }

    private void handleHttpResponseException(HttpResponseException e)
            throws CredentialInvalidException, HttpResponseException {
        if (e.getStatusCode() == 401) {
            throw new CredentialInvalidException();
        } else {
            throw e;
        }
    }

    private KeyStore loadKeyStore()
            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
        InputStream is = null;
        try {
            KeyStore ks = KeyStore.getInstance("JKS");
            is = getClass().getClassLoader().getResourceAsStream("trustStore.jks");
            if (is == null) {
                throw new RuntimeException();
            }
            ks.load(is, "Vertretungsplan".toCharArray());
            return ks;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    String getClassName(String text, JSONObject data) throws JSONException {
        text = text.replace("(", "").replace(")", "");
        if (data.has(PARAM_CLASS_REGEX)) {
            Pattern pattern = Pattern.compile(data.getString(PARAM_CLASS_REGEX));
            Matcher matcher = pattern.matcher(text);
            if (matcher.find()) {
                if (matcher.groupCount() > 0) {
                    return matcher.group(1);
                } else {
                    return matcher.group();
                }
            } else {
                return "";
            }
        } else {
            return text;
        }
    }

    protected boolean contains(JSONArray array, String string) throws JSONException {
        for (int i = 0; i < array.length(); i++) {
            if (array.getString(i).equals(string)) {
                return true;
            }
        }
        return false;
    }

    @Nullable
    protected List<String> getClassesFromJson() throws JSONException {
        final JSONObject data = scheduleData.getData();
        return ParserUtils.getClassesFromJson(data);
    }

    private class CustomHostnameVerifier implements HostnameVerifier {
        private String host;
        private DefaultHostnameVerifier defaultHostnameVerifier;

        public CustomHostnameVerifier(String host) {
            this.host = host;
            this.defaultHostnameVerifier = new DefaultHostnameVerifier();
        }

        @Override
        public boolean verify(String s, SSLSession sslSession) {
            return defaultHostnameVerifier.verify(host, sslSession)
                    | defaultHostnameVerifier.verify(this.host, sslSession);
        }
    }

    public boolean isPersonal() {
        return false;
    }
}