package com.love.apps.BT4U.webservice; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.mapper.MapperWrapper; /** * Provides an object model for interfacing with the BT4U web service, allowing * users to avoid dealing with XML, SOAP, etc and focus instead on requesting * and receiving information from the service. All methods are synchronous and * will block the calling thread until the request succeeds or times out <br /> * <br /> * All methods are guaranteed not to return null. On error empty data containers * may be returned, such as empty lists. <br> * There are two main time-scales of interest when discussing the webservice: * active and real time. As far as I can tell, active time implies any * information that's currently in the BT4U database with no filters applied for * the precise day, hour, or minute that the API call was made. Real-time, on * the other hand, pays attention to the exact time the request was made and * returns either filters or updated information<br /> * <br /> * <h2>Architecture</h2> Currently all calls do 1) connect to the server 2) * buffer the entire response into one big string 3) return that string (in * memory) for XML parsing. Obviously this is a relatively memory-intensive * process, especially if there are many requests running simultaneously. Future * versions could be improved by building and parsing the DOM as it is loaded * from the network, perhaps by combining the built-in XML pull parser and * Xstream. There are also substantial XML parsing improvements that could be * made to make the entire operation lighter and faster, such as actively * deciding on Xstream DOM readers and converters<br /> * <br /> * Web service located at: http://www.bt4u.org/BT4U_WebService.asmx * * * @author Hamilton Turner * */ public class BT4U { public static BT4U getService() { return new BT4U(); } /** * Synchronously contacts the web service and requests the latest * information on all buses currently operating */ public List<Bus> getRunningBuses() { XStream x = new XStream(); x.processAnnotations(Bus.class); x.alias("DocumentElement", BusCollection.class); x.addImplicitArray(BusCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetCurrentBusInfo"); return ((BusCollection) x.fromXML(xml)).data; } static private class BusCollection { public ArrayList<Bus> data = new ArrayList<Bus>(); } /** * Synchronously contacts the web service and gets all route codes (e.g. HWD * for Hethwood) for currently active routes */ public List<String> getActiveRouteCodes() { XStream x = new XStream(); x.alias("DocumentElement", RouteCollection.class); x.processAnnotations(CurrentRoute.class); x.addImplicitArray(RouteCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetCurrentRoutes"); ArrayList<String> names = new ArrayList<String>(); for (CurrentRoute route : ((RouteCollection) x.fromXML(xml)).data) names.add(route.name); return names; } @XStreamAlias("CurrentRoutes") private static class CurrentRoute { @XStreamAlias("RouteShortName") String name; } private static class RouteCollection { ArrayList<CurrentRoute> data = new ArrayList<BT4U.CurrentRoute>(); } /** * Given the short name of a route, such as "HWD", this will return all * stops that are scheduled on that route. <br /> * <br /> * Behavior is not consistent if a) that route is not currently running, or * b) the provided short code does not exist. In some cases an empty list * will be returned, and in others a full list will be returned. It seems as * though the intended behavior is to always return stop codes if the * provided route is running during this time of year (e.g. perhaps not * immediately running, but running sometime this week), and return no stop * codes if the provided route doesn't exist in the active database */ public List<ScheduledStop> getScheduledStopsForRoute(String routeCode) { XStream x = new XStream(); x.processAnnotations(ScheduledStop.class); x.alias("DocumentElement", StopCollection.class); x.addImplicitArray(StopCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetScheduledStopCodes?routeShortName=" + routeCode); return ((StopCollection) x.fromXML(xml)).data; } private static class StopCollection { ArrayList<ScheduledStop> data = new ArrayList<ScheduledStop>(); } /** * For a single stop on a single route, get all scheduled departures from * now until the end of service today. I do not know if this is simply a * relevant subset of the route timetable, or if this is a collection of * updated predictions based on the current location / time of the bus. I * suspect the latter. * * @param routeShortName * The 2-3 character short name for a route, e.g. HWD * @param routeStopCode * The stop code for a specific stop e.g. 1103 * @return Empty list if the provided route code / name do not exist in the * active database */ public List<ScheduledDeparture> getScheduledDeparturesFromStop( String routeShortName, int routeStopCode) { XStream x = new XStream(); x.processAnnotations(ScheduledDeparture.class); x.alias("DocumentElement", DepartureCollection.class); x.addImplicitArray(DepartureCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetNextDepartures?routeShortName=" + routeShortName + "&stopCode=" + routeStopCode); List<ScheduledDeparture> departures = ((DepartureCollection) x .fromXML(xml)).data; for (ScheduledDeparture sd : departures) sd.cleanup(); return departures; } private static class DepartureCollection { ArrayList<ScheduledDeparture> data = new ArrayList<ScheduledDeparture>(); } /** * Synchronously downloads the real-time route information for all running * buses. * * @return an empty list if there is any error */ public List<Route> getAllActiveRoutePaths() { XmlPullParserFactory factory; try { factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(false); factory.setValidating(false); XmlPullParser xpp = factory.newPullParser(); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetScheduledPatternPoints?routeName=HWD"); HashMap<String, RouteBuilder> builders = new HashMap<String, RouteBuilder>(); xpp.setInput(new StringReader(xml)); int eventType = xpp.getEventType(); RouteBuilder currentBuilder = null; boolean in_rank = false, in_lat = false, in_lon = false; String rank = "", lat = "", lon = ""; while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { String tag = xpp.getName(); if (tag.startsWith("RSA")) { RouteBuilder builder = builders.get(tag); if (builder == null) { builder = new RouteBuilder(tag); builders.put(tag, builder); } currentBuilder = builder; } else if (tag.equals("Rank")) in_rank = true; else if (tag.equals("Latitude")) in_lat = true; else if (tag.equals("Longitude")) in_lon = true; } else if (eventType == XmlPullParser.END_TAG) { String tag = xpp.getName(); if (tag.startsWith("RSA")) { currentBuilder.addPoint(rank, lat, lon); currentBuilder = null; rank = ""; lat = ""; lon = ""; } else if (tag.equals("Rank")) in_rank = false; else if (tag.equals("Latitude")) in_lat = false; else if (tag.equals("Longitude")) in_lon = false; } else if (eventType == XmlPullParser.TEXT) { if (in_lat) lat = xpp.getText(); else if (in_lon) lon = xpp.getText(); else if (in_rank) rank = xpp.getText(); } eventType = xpp.next(); } List<Route> routes = new ArrayList<Route>(builders.size()); for (RouteBuilder rb : builders.values()) routes.add(rb.build()); return routes; } catch (XmlPullParserException e) { e.printStackTrace(); return new ArrayList<Route>(); } catch (IOException e) { e.printStackTrace(); return new ArrayList<Route>(); } } /** * Given a specific stop code, this informs you which routes operate on this * stop code. This is not filtered to include routes running only in * real-time, but includes all active routes for a given stop id * * @param stop * The ID of that stop, such at 1103 * @return */ public List<String> getRouteCodesForStop(int stop) { XStream x = new XStream(); x.processAnnotations(ScheduledRoute.class); x.alias("DocumentElement", ScheduledRouteCollection.class); x.addImplicitArray(ScheduledRouteCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetScheduledRoutes?stopCode=" + stop); ScheduledRouteCollection rc = (ScheduledRouteCollection) x.fromXML(xml); ArrayList<String> routes = new ArrayList<String>(); for (ScheduledRoute r : rc.data) routes.add(r.routeCode); return routes; } /** * May actually be performing the same function as * {@link BT4U#getScheduledStopsForRoute(String)} * * @param route * @return */ public List<BusStop> getStopsForRoute(String route) { XStream x = new XStream(); x.processAnnotations(BusStop.class); x.alias("DocumentElement", BusStopCollection.class); x.addImplicitArray(BusStopCollection.class, "data"); String xml = fetchXML("http://www.bt4u.org/BT4U_WebService.asmx/GetScheduledStopNames?routeShortName=" + route); return ((BusStopCollection) x.fromXML(xml)).data; } private static class BusStopCollection { List<BusStop> data = new ArrayList<BusStop>(); } private static class ScheduledRouteCollection { ArrayList<ScheduledRoute> data = new ArrayList<BT4U.ScheduledRoute>(); } @XStreamAlias("ScheduledRoutes") private static class ScheduledRoute { @XStreamAlias("RouteShortName") String routeCode = ""; } /** * Synchronously downloads the XML from the given web service URL. * * @param url * @return */ private String fetchXML(String url) { String xml = ""; try { HttpClient httpClient = new DefaultHttpClient(); HttpGet httpRequest = new HttpGet(url); HttpResponse response; response = httpClient.execute(httpRequest); InputStream in = response.getEntity().getContent(); InputStreamReader ir = new InputStreamReader(in); BufferedReader bin = new BufferedReader(ir); String line = null; StringBuffer buff = new StringBuffer(); while ((line = bin.readLine()) != null) { buff.append(line).append("\n"); } bin.close(); xml = buff.toString(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return xml; } /** * Return a generically-configured future-proof Xstream object. This should * ignore unknown XML elements to protect the parsing code from future * enhancements. Do not use for testing! * * TODO: Implement and test */ @SuppressWarnings("unused") private static XStream getXStream() { XStream xstream = new XStream() { @Override protected MapperWrapper wrapMapper(MapperWrapper next) { return new MapperWrapper(next) { @Override public boolean shouldSerializeMember( @SuppressWarnings("rawtypes") Class definedIn, String fieldName) { if (definedIn == Object.class) { return false; } return super .shouldSerializeMember(definedIn, fieldName); } }; } }; return xstream; } }