fr.outadev.android.timeo.TimeoRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for fr.outadev.android.timeo.TimeoRequestHandler.java

Source

/*
 * Twistoast - KeolisRequestHandler
 * Copyright (C) 2013-2014  Baptiste Candellier
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.outadev.android.timeo;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;

import com.github.kevinsawicki.http.HttpRequest;
import com.github.kevinsawicki.http.HttpRequest.HttpRequestException;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Handles all connections to the Twisto Realtime API.
 *
 * @author outadoc
 */
public abstract class TimeoRequestHandler {

    public final static String TAG = "Timeo";

    public final static int DEFAULT_NETWORK_CODE = 147;
    private final static int REQUEST_TIMEOUT = 10000;

    private final static String API_BASE_URL = "http://timeo3.keolis.com/relais/";
    private final static String PRE_HOME_URL = "http://twisto.fr/module/mobile/App2014/utils/getPreHome.php";

    /**
     * Requests a web page via an HTTP GET request.
     *
     * @param url       URL to fetch
     * @param params    HTTP GET parameters as a string (e.g. foo=bar&bar=foobar)
     * @param useCaches true if the client can cache the request
     * @return the raw body of the page
     * @throws HttpRequestException if an HTTP error occurred
     */
    private static String requestWebPage(String url, String params, boolean useCaches) throws HttpRequestException {
        String finalUrl = url + "?" + params;
        Log.i(TAG, "requesting " + finalUrl);

        return HttpRequest.get(finalUrl).useCaches(useCaches).readTimeout(REQUEST_TIMEOUT).body();
    }

    /**
     * Requests a web page via an HTTP GET request.
     *
     * @param url       URL to fetch
     * @param useCaches true if the client can cache the request
     * @return the raw body of the page
     * @throws HttpRequestException if an HTTP error occurred
     */
    private static String requestWebPage(String url, boolean useCaches) throws HttpRequestException {
        return requestWebPage(url, "", useCaches);
    }

    /**
     * Shorthand methods for requesting data from the default city's API (Twisto/Caen)
     */

    /**
     * Fetch the bus lines from the API.
     *
     * @return a list of lines
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoLine> getLines()
            throws HttpRequestException, XmlPullParserException, IOException, TimeoException {
        return getLines(DEFAULT_NETWORK_CODE);
    }

    /**
     * Fetch a list of bus stops from the API.
     *
     * @param line the line for which we should fetch the stops
     * @return a list of bus stops
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoStop> getStops(TimeoLine line)
            throws HttpRequestException, XmlPullParserException, IOException, TimeoException {
        return getStops(line.getNetworkCode(), line);
    }

    /**
     * Fetches a schedule for a single bus stop from the API.
     *
     * @param stop the bus stop to fetch the schedule for
     * @return a TimeoStopSchedule containing said schedule
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static TimeoStopSchedule getSingleSchedule(TimeoStop stop)
            throws HttpRequestException, TimeoException, IOException, XmlPullParserException {
        return getSingleSchedule(stop.getLine().getNetworkCode(), stop);
    }

    /**
     * Fetches schedules for multiple bus stops from the API.
     *
     * @param stops a list of bus stops we should fetch the schedules for
     * @return a list of TimeoStopSchedule containing said schedules
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoStopSchedule> getMultipleSchedules(List<TimeoStop> stops)
            throws HttpRequestException, TimeoException, XmlPullParserException, IOException {
        //if we don't specify any network code when calling getMultipleSchedules, we'll have to figure them out ourselves.
        //we can only fetch a list of schedules that are all part of the same network.
        //therefore, we'll have to separate them in different lists and request them individually.

        //a list of all the different network codes we'll have to check
        List<Integer> networks = new ArrayList<>();
        //the final list that will contain all of our schedules
        List<TimeoStopSchedule> finalScheduleList = new ArrayList<>();

        //list all the required network codes, and add them to the list
        for (TimeoStop stop : stops) {
            if (!networks.contains(stop.getLine().getNetworkCode())) {
                networks.add(stop.getLine().getNetworkCode());
            }
        }

        Log.i(TAG, networks.size() + " different bus networks to refresh");

        //for each network
        for (Integer network : networks) {
            List<TimeoStop> stopsForThisNetwork = new ArrayList<>();

            //get the list of stops we'll have to request
            for (TimeoStop stop : stops) {
                if (stop.getLine().getNetworkCode() == network) {
                    stopsForThisNetwork.add(stop);
                }
            }

            //request the schedules and add them to the final list
            finalScheduleList.addAll(getMultipleSchedules(network, stopsForThisNetwork));
        }

        return finalScheduleList;
    }

    /**
     * Fetch the bus lines from the API.
     *
     * @param networkCode the code for the city's bus network
     * @return a list of lines
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoLine> getLines(int networkCode)
            throws HttpRequestException, XmlPullParserException, IOException, TimeoException {
        String params = "xml=1";
        String result = requestWebPage(API_BASE_URL + getPageNameForNetworkCode(networkCode), params, true);

        XmlPullParser parser = getParserForXMLString(result);
        int eventType = parser.getEventType();

        TimeoLine tmpLine = null;
        TimeoIDNameObject tmpDirection;

        ArrayList<TimeoLine> lines = new ArrayList<>();

        String errorCode = null;
        String text = null;

        boolean isInLineTag = false;

        while (eventType != XmlPullParser.END_DOCUMENT) {
            String tagname = parser.getName();

            switch (eventType) {
            case XmlPullParser.START_TAG:

                if (tagname.equals("ligne")) {
                    isInLineTag = true;
                    tmpDirection = new TimeoIDNameObject();
                    tmpLine = new TimeoLine(new TimeoIDNameObject(), tmpDirection, networkCode);

                } else if (tagname.equals("arret")) {
                    isInLineTag = false;

                } else if (tagname.equals("erreur")) {
                    errorCode = parser.getAttributeValue(null, "code");
                }

                break;

            case XmlPullParser.TEXT:
                text = parser.getText();
                break;

            case XmlPullParser.END_TAG:
                if (tagname.equals("ligne")) {
                    lines.add(tmpLine);

                } else if (tmpLine != null && tagname.equals("code") && isInLineTag) {
                    tmpLine.getDetails().setId(text);

                } else if (tmpLine != null && tagname.equals("nom") && isInLineTag) {
                    tmpLine.getDetails().setName(smartCapitalize(text));

                } else if (tmpLine != null && tagname.equals("sens") && isInLineTag) {
                    tmpLine.getDirection().setId(text);

                } else if (tmpLine != null && tagname.equals("vers") && isInLineTag) {
                    tmpLine.getDirection().setName(smartCapitalize(text));

                } else if (tmpLine != null && tagname.equals("couleur") && isInLineTag) {
                    tmpLine.setColor("#" + StringUtils.leftPad(Integer.toHexString(Integer.valueOf(text)), 6, '0'));

                } else if (tagname.equals("erreur")) {
                    if ((errorCode != null && !errorCode.equals("000"))
                            || (text != null && !text.trim().isEmpty())) {
                        throw new TimeoException(errorCode + " - " + text);
                    }
                }

                break;

            default:
                break;
            }

            eventType = parser.next();
        }

        return lines;
    }

    /**
     * Fetch a list of bus stops from the API.
     *
     * @param networkCode the code for the city's bus network
     * @param line        the line for which we should fetch the stops
     * @return a list of bus stops
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoStop> getStops(int networkCode, TimeoLine line)
            throws HttpRequestException, XmlPullParserException, IOException, TimeoException {
        String params = "xml=1&ligne=" + line.getId() + "&sens=" + line.getDirection().getId();
        String result = requestWebPage(API_BASE_URL + getPageNameForNetworkCode(networkCode), params, true);

        XmlPullParser parser = getParserForXMLString(result);
        int eventType = parser.getEventType();

        TimeoStop tmpStop = null;
        ArrayList<TimeoStop> stops = new ArrayList<>();

        String errorCode = null;
        String text = null;

        boolean isInStopTag = true;

        while (eventType != XmlPullParser.END_DOCUMENT) {
            String tagname = parser.getName();

            switch (eventType) {
            case XmlPullParser.START_TAG:

                if (tagname.equals("ligne")) {
                    isInStopTag = false;

                } else if (tagname.equals("arret")) {
                    isInStopTag = true;
                    tmpStop = new TimeoStop(line);

                } else if (tagname.equals("erreur")) {
                    errorCode = parser.getAttributeValue(null, "code");
                }

                break;

            case XmlPullParser.TEXT:
                text = parser.getText();
                break;

            case XmlPullParser.END_TAG:
                if (tagname.equals("als")) {
                    stops.add(tmpStop);

                } else if (tmpStop != null && tagname.equals("code") && isInStopTag) {
                    tmpStop.setId(text);

                } else if (tmpStop != null && tagname.equals("nom") && isInStopTag) {
                    tmpStop.setName(smartCapitalize(text));

                } else if (tmpStop != null && tagname.equals("refs")) {
                    tmpStop.setReference(text);

                } else if (tagname.equals("erreur")) {
                    if ((errorCode != null && !errorCode.equals("000"))
                            || (text != null && !text.trim().isEmpty())) {
                        throw new TimeoException(errorCode + " - " + text);
                    }
                }

                break;

            default:
                break;
            }

            eventType = parser.next();
        }

        return stops;
    }

    /**
     * Fetches a schedule for a single bus stop from the API.
     *
     * @param networkCode the code for the city's bus network
     * @param stop        the bus stop to fetch the schedule for
     * @return a TimeoStopSchedule containing said schedule
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static TimeoStopSchedule getSingleSchedule(int networkCode, TimeoStop stop)
            throws HttpRequestException, TimeoException, IOException, XmlPullParserException {
        List<TimeoStop> list = new ArrayList<>();
        list.add(stop);
        List<TimeoStopSchedule> schedules = getMultipleSchedules(networkCode, list);

        if (schedules.size() > 0) {
            return schedules.get(0);
        } else {
            throw new TimeoException("No schedules were returned.");
        }
    }

    /**
     * Fetches schedules for multiple bus stops from the API.
     *
     * @param networkCode the code for the city's bus network
     * @param stops       a list of bus stops we should fetch the schedules for
     * @return a list of TimeoStopSchedule containing said schedules
     * @throws HttpRequestException   if an HTTP error occurred
     * @throws XmlPullParserException if a parsing exception occurred
     * @throws IOException            if an I/O exception occurred whilst parsing the XML
     * @throws TimeoException         if the API returned an error
     */
    @NonNull
    public static List<TimeoStopSchedule> getMultipleSchedules(int networkCode, List<TimeoStop> stops)
            throws HttpRequestException, TimeoException, XmlPullParserException, IOException {
        //final schedules to return
        List<TimeoStopSchedule> schedules = new ArrayList<>();

        //the list of stop references we'll be sending to the api
        String refs = "";

        //append the stop references to the refs
        for (TimeoStop stop : stops) {
            if (stop.getReference() != null) {
                refs += stop.getReference() + ";";
            }
        }

        //if no stops are in the list or all refs are null
        if (stops.isEmpty() || refs.isEmpty()) {
            return schedules;
        }

        //don't keep the last semicolon
        refs = refs.substring(0, refs.length() - 1);

        String params = "xml=3&refs=" + refs + "&ran=1";
        String result = requestWebPage(API_BASE_URL + getPageNameForNetworkCode(networkCode), params, false);

        //create a new parser
        XmlPullParser parser = getParserForXMLString(result);
        int eventType = parser.getEventType();

        //temporary schedule (associated with a stop and a few schedules)
        TimeoStopSchedule tmpSchedule = null;
        //temporary single schedule (one time, one destination)
        TimeoSingleSchedule tmpSingleSchedule = null;
        //temporary blocking exception, in case we meet a blocking network message
        TimeoBlockingMessageException tmpBlockingException = null;

        String tmpLineId = null;
        String tmpStopId = null;

        String errorCode = null;

        //text will contain the last text value we encountered during the parsing
        String text = null;

        //parse the whole document
        while (eventType != XmlPullParser.END_DOCUMENT) {
            String tagname = parser.getName();

            switch (eventType) {
            case XmlPullParser.START_TAG:

                if (tagname.equals("horaire")) {
                    //if this is the start of the stop schedule's list, instantiate it with empty values
                    tmpSchedule = new TimeoStopSchedule();

                } else if (tagname.equals("passage")) {
                    //if this is a new schedule time
                    tmpSingleSchedule = new TimeoSingleSchedule();

                } else if (tagname.equals("reseau")) {
                    //if this is a network-wide traffic message
                    tmpBlockingException = new TimeoBlockingMessageException();

                } else if (tagname.equals("erreur")) {
                    //if this is an API error, remember the error code
                    errorCode = parser.getAttributeValue(null, "code");
                }

                break;

            case XmlPullParser.TEXT:
                text = parser.getText();
                break;

            case XmlPullParser.END_TAG:
                if (tmpSchedule != null && tagname.equals("code")) {
                    tmpStopId = text;

                } else if (tmpSchedule != null && tagname.equals("ligne")) {
                    tmpLineId = text;

                } else if (tmpSchedule != null && tagname.equals("sens")) {

                    //try to find the stop we're currently looking at in the list
                    for (TimeoStop i : stops) {
                        //if this one has got the right stop id, line id, and direction id, looks like we've found it
                        if (i.getId().equals(tmpStopId) && i.getLine().getId().equals(tmpLineId)
                                && i.getLine().getDirection().getId().equals(text)) {
                            //remember the stop
                            tmpSchedule.setStop(i);
                        }
                    }

                    //if we didn't find it, just set the schedule to null and carry on.
                    if (tmpSchedule.getStop() == null) {
                        tmpSchedule = null;
                    }

                } else if (tmpSingleSchedule != null && tagname.equals("duree")) {
                    tmpSingleSchedule.setTime(text);

                } else if (tmpSingleSchedule != null && tagname.equals("destination")) {
                    tmpSingleSchedule.setDirection(smartCapitalize(text));

                } else if (tmpSingleSchedule != null && tmpSchedule != null && tagname.equals("passage")) {
                    tmpSchedule.getSchedules().add(tmpSingleSchedule);

                } else if (tmpSchedule != null && tagname.equals("horaire")) {
                    schedules.add(tmpSchedule);

                } else if (tagname.equals("erreur")) {
                    if ((errorCode != null && !errorCode.equals("000"))
                            || (text != null && !text.trim().isEmpty())) {
                        throw new TimeoException(errorCode + " - " + text);
                    }

                } else if (tmpBlockingException != null && tagname.equals("titre") && !text.isEmpty()) {
                    tmpBlockingException.setMessageTitle(text);

                } else if (tmpBlockingException != null && tagname.equals("texte") && !text.isEmpty()) {
                    tmpBlockingException.setMessageBody(text);

                } else if (tmpBlockingException != null && tagname.equals("bloquant") && text.equals("true")) {
                    //if we met with a blocking network-wide traffic message
                    throw tmpBlockingException;
                }

                break;

            default:
                break;
            }

            eventType = parser.next();
        }

        return schedules;
    }

    /**
     * Checks if there are outdated stops amongst those in the database,
     * by comparing them to a list of schedules returned by the API.
     * <p/>
     * The isOutdated property of the bus stops will be set accordingly.
     *
     * @param stops     a list of bus stops to check. their isOutdated property may be modified
     * @param schedules a list of schedules returned by the API and corresponding to the stops
     * @return the number of outdated stops that have been found
     * @throws TimeoException if stops or schedules is null
     */
    public static int checkForOutdatedStops(List<TimeoStop> stops, List<TimeoStopSchedule> schedules)
            throws TimeoException {
        if (stops == null || schedules == null) {
            throw new TimeoException();
        }

        if (stops.size() == schedules.size()) {
            return 0;
        }

        int count = 0;

        for (TimeoStop stop : stops) {
            boolean outdated = true;

            for (TimeoStopSchedule schedule : schedules) {
                if (schedule.getStop() == stop) {
                    outdated = false;
                    count++;
                    break;
                }
            }

            stop.setOutdated(outdated);
        }

        return count;
    }

    /**
     * Fetches the current global traffic alert message. Might or might not be null.
     *
     * @return a TimeoTrafficAlert if an alert is currently broadcasted on the website, else null
     */
    @Nullable
    public static TimeoTrafficAlert getGlobalTrafficAlert() {
        String source = requestWebPage(PRE_HOME_URL, true);

        if (source != null && !source.isEmpty()) {
            try {
                JSONObject obj = (JSONObject) new JSONTokener(source).nextValue();

                if (obj.has("alerte")) {
                    JSONObject alert = obj.getJSONObject("alerte");
                    return new TimeoTrafficAlert(alert.getInt("id_alerte"), alert.getString("libelle_alerte"),
                            alert.getString("url_alerte"));
                }
            } catch (JSONException e) {
                return null;
            }
        }

        return null;
    }

    /**
     * Capitalizes the first letter of every word, like WordUtils.capitalize(); except it does it WELL.
     * The determinants will not be capitalized, whereas some acronyms will.
     *
     * @param str The text to capitalize.
     * @return The capitalized text.
     */
    public static String smartCapitalize(String str) {
        String newStr = "";
        str = str.toLowerCase().trim();

        //these words will never be capitalized
        String[] determinants = new String[] { "de", "du", "des", "au", "aux", "", "la", "le", "les", "d", "et",
                "l" };
        //these words will always be capitalized
        String[] specialWords = new String[] { "sncf", "chu", "chr", "chs", "crous", "suaps", "fpa", "za", "zi",
                "zac", "cpam", "efs", "mjc" };

        //explode the string with both spaces and apostrophes
        String[] words = str.split("( |\\-|'|\\/)");

        for (String word : words) {
            if (Arrays.asList(determinants).contains(word)) {
                //if the word should not be capitalized, just append it to the new string
                newStr += word;
            } else if (Arrays.asList(specialWords).contains(word)) {
                //if the word should be in upper case, do eet
                newStr += word.toUpperCase(Locale.FRENCH);
            } else {
                //if it's a normal word, just capitalize it
                newStr += StringUtils.capitalize(word);
            }

            try {
                //we don't know if the next character is a blank space or an apostrophe, so we check that
                char delimiter = str.charAt(newStr.length());
                newStr += delimiter;
            } catch (StringIndexOutOfBoundsException ignored) {
                //will be thrown for the last word of the string
            }
        }

        return StringUtils.capitalize(newStr);
    }

    /**
     * Gets an XmlPullParser for an XML string.
     *
     * @param xml an xml document in a String, sexy
     * @return an XmlPullParser ready to parse the document
     */
    private static XmlPullParser getParserForXMLString(String xml) throws XmlPullParserException, IOException {
        XmlPullParser parser = Xml.newPullParser();

        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
        parser.setInput(new StringReader(xml));
        parser.nextTag();

        return parser;
    }

    /**
     * Gets the list of the supported bus networks.
     *
     * @return an array containing the network names; the index is their code, and they're associated with their name
     */
    public static SparseArray<String> getNetworksList() {
        SparseArray<String> networks = new SparseArray<>();

        networks.put(105, "Le Mans");
        networks.put(117, "Pau");
        networks.put(120, "Soissons");
        networks.put(135, "Aix-en-Provence");
        networks.put(147, "Caen");
        networks.put(217, "Dijon");
        networks.put(297, "Brest");
        networks.put(402, "Pau-Agen");
        networks.put(416, "Blois");
        networks.put(422, "Saint-tienne");
        networks.put(440, "Nantes");
        networks.put(457, "Montargis");
        networks.put(497, "Angers");
        networks.put(691, "Macon-Villefranche");
        networks.put(910, "pinay-sur-Orge");
        networks.put(999, "Rennes");

        return networks;
    }

    /**
     * Returns the API endpoint for a given network code.
     *
     * @param networkCode the code for the city's bus network
     * @return the name of the page that has to be called for this specific network
     */
    private static String getPageNameForNetworkCode(int networkCode) {
        return networkCode + ".php";
    }

}