/*
* Copyright (C) 2011 - 2012 Niall 'Rivernile' Scott
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors or contributors be held liable for
* any damages arising from the use of this software.
*
* The aforementioned copyright holder(s) hereby grant you a
* non-transferrable right to use this software for any purpose (including
* commercial applications), and to modify it and redistribute it, subject to
* the following conditions:
*
* 1. This notice may not be removed or altered from any file it appears in.
*
* 2. Any modifications made to this software, except those defined in
* clause 3 of this agreement, must be released under this license, and
* the source code of any modifications must be made available on a
* publically accessible (and locateable) website, or sent to the
* original author of this software.
*
* 3. Software modifications that do not alter the functionality of the
* software but are simply adaptations to a specific environment are
* exempt from clause 2.
*/
package uk.org.rivernile.edinburghbustracker.android.livetimes.parser;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Random;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusParser;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusParserException;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusStop;
import uk.org.rivernile.edinburghbustracker.android.ApiKey;
/**
* This is the Edinburgh specific implementation of the bus times parser. To
* get an instance of this class, call EdinburghParser.getInstance()
*
* @author Niall Scott
*/
public final class EdinburghParser implements BusParser {
/** This error is called when an invalid key has been specified. */
public static final byte ERROR_INVALID_APP_KEY = 7;
/** This error is called when an invalid parameter has been specified. */
public static final byte ERROR_INVALID_PARAMETER = 8;
/** This error is called when the system encounters a processing error. */
public static final byte ERROR_PROCESSING_ERROR = 9;
/** This error is called when the system is under maintenance. */
public static final byte ERROR_SYSTEM_MAINTENANCE = 10;
/** This error is called when the system is overloaded. */
public static final byte ERROR_SYSTEM_OVERLOADED = 11;
private static final String URL =
"http://www.mybustracker.co.uk/ws.php?module=json&key=";
private static final Random rand = new Random(System.currentTimeMillis());
private boolean globalDisruption = false;
/**
* Create a new EdinburghParser object.
*/
public EdinburghParser() {
// Nothing to do here.
}
/**
* {@inheritDoc}
*/
@Override
public HashMap<String, BusStop> getBusStopData(final String[] stopCodes,
final int numDepartures) throws BusParserException {
if(stopCodes == null || stopCodes.length == 0) return null;
// Build the URL.
final StringBuilder sb = new StringBuilder();
sb.append(URL);
sb.append(ApiKey.getHashedKey());
sb.append("&function=getBusTimes&");
final int len = stopCodes.length;
if(len == 1) {
sb.append("stopId=");
sb.append(stopCodes[0]);
sb.append('&');
} else {
for(int i = 0; i < len; i++) {
if(i >= 6) break;
sb.append("stopId");
sb.append(i + 1);
sb.append('=');
sb.append(stopCodes[i]);
sb.append('&');
}
}
sb.append("nb=");
sb.append(numDepartures);
// Add a random arg so the response isn't cached by the network proxies.
sb.append("&random=");
sb.append(rand.nextInt());
// TODO: review this code. I'm sure it could be done better.
try {
final URL url = new URL(sb.toString());
// Reset the StringBuilder because we're going to reuse it.
sb.setLength(0);
final HttpURLConnection conn = (HttpURLConnection)url
.openConnection();
try {
final BufferedInputStream is = new BufferedInputStream(
conn.getInputStream());
// Check to see if the URL we connected to was what we expected.
if(!url.getHost().equals(conn.getURL().getHost())) {
is.close();
conn.disconnect();
throw new BusParserException(ERROR_URLMISMATCH);
}
int data;
while((data = is.read()) != -1) {
sb.append((char)data);
}
} finally {
conn.disconnect();
}
return parseJSON(sb.toString());
} catch(MalformedURLException e) {
throw new BusParserException(ERROR_CANNOTRESOLVE);
} catch(IOException e) {
throw new BusParserException(ERROR_NOCONNECTION);
} catch(JSONException e) {
throw new BusParserException(ERROR_PARSEERR);
}
}
/**
* Parse the JSON string returned from the bus tracker web services.
*
* @param jsonString The JSON string to parse.
* @return A HashMap which has String -> BusStop mappings containing the
* bus stop data.
* @throws JSONException When a JSON exception occurs.
* @throws BusParserException When a BusParserException occurs.
*/
private HashMap<String, BusStop> parseJSON(final String jsonString)
throws JSONException, BusParserException {
final HashMap<String, BusStop> data = new HashMap<String, BusStop>();
JSONObject jo = new JSONObject(jsonString);
// Check to see if the API returns errors.
if(jo.has("faultcode")) {
final String err = jo.getString("faultcode");
if("INVALID_APP_KEY".equals(err)) {
throw new BusParserException(ERROR_INVALID_APP_KEY);
} else if("INVALID_PARAMETER".equals(err)) {
throw new BusParserException(ERROR_INVALID_PARAMETER);
} else if("PROCESSING_ERROR".equals(err)) {
throw new BusParserException(ERROR_PROCESSING_ERROR);
} else if("SYSTEM_MAINTENANCE".equals(err)) {
throw new BusParserException(ERROR_SYSTEM_MAINTENANCE);
} else if("SYSTEM_OVERLOADED".equals(err)) {
throw new BusParserException(ERROR_SYSTEM_OVERLOADED);
} else {
throw new BusParserException(ERROR_UNKNOWN);
}
}
final JSONArray ja = jo.getJSONArray("busTimes");
EdinburghBusStop currentBusStop;
EdinburghBusService currentBusService;
// Make sure there's array elements.
final int len = ja.length();
if(len == 0) {
throw new BusParserException(ERROR_NODATA);
}
String temp;
for(int i = 0; i < len; i++) {
jo = ja.getJSONObject(i);
// Check to see if there are any global disruptions.
globalDisruption = jo.getBoolean("globalDisruption");
temp = jo.getString("stopId");
// Get data for the bus stop.
currentBusStop = (EdinburghBusStop)data.get(temp);
if(currentBusStop == null) {
currentBusStop = parseEdinburghBusStop(jo);
if(currentBusStop != null) {
data.put(temp, currentBusStop);
} else {
continue;
}
}
// Add a bus service to the current bus stop.
currentBusService = parseEdinburghBusService(jo);
if(currentBusService != null) {
currentBusStop.addBusService(currentBusService);
}
}
return data;
}
/**
* Create an EdinburghBusStop object from a JSONObject.
*
* @param joStop The JSONObject to parse.
* @return An EdinburghBusStop object, or null if there was a problem.
*/
private static EdinburghBusStop parseEdinburghBusStop(
final JSONObject joStop) {
try {
return new EdinburghBusStop(joStop.getString("stopId"),
joStop.getString("stopName"),
joStop.getBoolean("busStopDisruption"));
} catch(JSONException e) {
// Nothing to do.
} catch(IllegalArgumentException e) {
// Nothing to do.
}
return null;
}
/**
* Create an EdinburghBus object from a JSONObject.
*
* @param joService The JSONObject to parse.
* @return An EdinburghBusService object, or null if there was a problem.
*/
private static EdinburghBusService parseEdinburghBusService(
final JSONObject joService) {
try {
final String serviceName = serviceNameConversion(
joService.getString("mnemoService"));
final EdinburghBusService service = new EdinburghBusService(
serviceName, joService.getString("nameService"),
joService.getBoolean("serviceDisruption"));
final JSONArray jaBuses = joService.getJSONArray("timeDatas");
final int len = jaBuses.length();
EdinburghBus currentBus;
// Loop through the times for each bus.
for(int i = 0; i < len; i++) {
currentBus = parseEdinburghBus(jaBuses.getJSONObject(i));
if(currentBus != null) {
service.addBus(currentBus);
}
}
return service;
} catch(JSONException e) {
// Nothing to do.
} catch(IllegalArgumentException e) {
// Nothing to do.
}
return null;
}
/**
* Create an EdinburghBus object from a JSONObject.
*
* @param joBus The JSONObject to parse.
* @return An EdinburghBus object, or null if there was a problem.
*/
private static EdinburghBus parseEdinburghBus(final JSONObject joBus) {
try {
final char reliability = joBus.getString("reliability").charAt(0);
final char type = joBus.getString("type").charAt(0);
return new EdinburghBus(joBus.getString("nameDest"),
joBus.getInt("day"), joBus.getString("time"),
joBus.getInt("minutes"), reliability, type,
joBus.getString("terminus"));
} catch(JSONException e) {
// Nothing to do.
} catch(IllegalArgumentException e) {
// Nothing to do.
}
return null;
}
/**
* Convert service names in to their public displays. For example, the tram
* isn't in the system as a tram.
*
* @param serviceName The service name to possibly convert.
* @return The converted service name, or the same service name if no
* conversion was required.
*/
private static String serviceNameConversion(final String serviceName) {
return "50".equals(serviceName) || "T50".equals(serviceName) ?
"TRAM" : serviceName;
}
/**
* Get the global disruption status.
*
* @return The global disruption status.
*/
public boolean getGlobalDisruption() {
return globalDisruption;
}
}