/* * Copyright 2010-2014 the original author or authors. * * 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 . */ package de.schildbach.pte; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import de.schildbach.pte.dto.Departure; import de.schildbach.pte.dto.Line; import de.schildbach.pte.dto.Location; import de.schildbach.pte.dto.LocationType; import de.schildbach.pte.dto.NearbyStationsResult; import de.schildbach.pte.dto.Point; import de.schildbach.pte.dto.Position; import de.schildbach.pte.dto.Product; import de.schildbach.pte.dto.QueryDeparturesResult; import de.schildbach.pte.dto.QueryTripsContext; import de.schildbach.pte.dto.QueryTripsResult; import de.schildbach.pte.dto.ResultHeader; import de.schildbach.pte.dto.StationDepartures; import de.schildbach.pte.dto.Stop; import de.schildbach.pte.dto.Trip; import de.schildbach.pte.exception.SessionExpiredException; import de.schildbach.pte.util.LittleEndianDataInputStream; import de.schildbach.pte.util.ParserUtils; import de.schildbach.pte.util.StringReplaceReader; import de.schildbach.pte.util.XmlPullUtil; /** * @author Andreas Schildbach */ public abstract class AbstractHafasProvider extends AbstractNetworkProvider { protected final static String SERVER_PRODUCT = "hafas"; private static final String REQC_PROD = "hafas"; protected final String stationBoardEndpoint; protected final String getStopEndpoint; protected final String queryEndpoint; private final int numProductBits; private String accessId; private String clientType; private Charset jsonGetStopsEncoding; private Charset jsonNearbyStationsEncoding; private final Charset xmlMlcResEncoding; private boolean dominantPlanStopTime = false; private boolean canDoEquivs = true; private boolean useIso8601 = false; private String extXmlEndpoint = null; private static class Context implements QueryTripsContext { public final String laterContext; public final String earlierContext; public final int sequence; public Context(final String laterContext, final String earlierContext, final int sequence) { this.laterContext = laterContext; this.earlierContext = earlierContext; this.sequence = sequence; } public boolean canQueryLater() { return laterContext != null; } public boolean canQueryEarlier() { return earlierContext != null; } } public static class QueryTripsBinaryContext implements QueryTripsContext { public final String ident; public final int seqNr; public final String ld; public final int usedBufferSize; public QueryTripsBinaryContext(final String ident, final int seqNr, final String ld, final int usedBufferSize) { this.ident = ident; this.seqNr = seqNr; this.ld = ld; this.usedBufferSize = usedBufferSize; } public boolean canQueryLater() { return true; } public boolean canQueryEarlier() { return true; } } public AbstractHafasProvider(final String stationBoardEndpoint, final String getStopEndpoint, final String queryEndpoint, final int numProductBits) { this(stationBoardEndpoint, getStopEndpoint, queryEndpoint, numProductBits, ISO_8859_1, ISO_8859_1); } public AbstractHafasProvider(final String stationBoardEndpoint, final String getStopEndpoint, final String queryEndpoint, final int numProductBits, final Charset jsonEncoding, final Charset xmlMlcResEncoding) { this.stationBoardEndpoint = stationBoardEndpoint; this.getStopEndpoint = getStopEndpoint; this.queryEndpoint = queryEndpoint; this.numProductBits = numProductBits; this.jsonGetStopsEncoding = jsonEncoding; this.jsonNearbyStationsEncoding = jsonEncoding; this.xmlMlcResEncoding = xmlMlcResEncoding; } protected void setClientType(final String clientType) { this.clientType = clientType; } protected void setAccessId(final String accessId) { this.accessId = accessId; } protected void setDominantPlanStopTime(final boolean dominantPlanStopTime) { this.dominantPlanStopTime = dominantPlanStopTime; } protected void setJsonGetStopsEncoding(final Charset jsonGetStopsEncoding) { this.jsonGetStopsEncoding = jsonGetStopsEncoding; } protected void setJsonNearbyStationsEncoding(final Charset jsonNearbyStationsEncoding) { this.jsonNearbyStationsEncoding = jsonNearbyStationsEncoding; } protected void setCanDoEquivs(final boolean canDoEquivs) { this.canDoEquivs = canDoEquivs; } protected void setUseIso8601(final boolean useIso8601) { this.useIso8601 = useIso8601; } protected void setExtXmlEndpoint(final String extXmlEndpoint) { this.extXmlEndpoint = extXmlEndpoint; } protected TimeZone timeZone() { return TimeZone.getTimeZone("CET"); } protected final String allProductsString() { final StringBuilder allProducts = new StringBuilder(numProductBits); for (int i = 0; i < numProductBits; i++) allProducts.append('1'); return allProducts.toString(); } protected final int allProductsInt() { return (1 << numProductBits) - 1; } protected char intToProduct(final int value) { return 0; } protected abstract void setProductBits(StringBuilder productBits, Product product); private static final Pattern P_SPLIT_ADDRESS = Pattern.compile("(\\d{4,5}\\s+[^,]+),\\s+(.*)"); protected String[] splitPlaceAndName(final String name) { final Matcher matcher = P_SPLIT_ADDRESS.matcher(name); if (matcher.matches()) return new String[] { matcher.group(1), matcher.group(2) }; else return new String[] { null, name }; } private final String wrapReqC(final CharSequence request, final Charset encoding) { return "" // + "" // + request // + ""; } private final Location parseStation(final XmlPullParser pp) { final String type = pp.getName(); if ("Station".equals(type)) { final String name = pp.getAttributeValue(null, "name").trim(); final String id = pp.getAttributeValue(null, "externalStationNr"); final int x = Integer.parseInt(pp.getAttributeValue(null, "x")); final int y = Integer.parseInt(pp.getAttributeValue(null, "y")); final String[] placeAndName = splitPlaceAndName(name); return new Location(LocationType.STATION, id, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private static final Location parsePoi(final XmlPullParser pp) { final String type = pp.getName(); if ("Poi".equals(type)) { String name = pp.getAttributeValue(null, "name").trim(); if (name.equals("unknown")) name = null; final int x = Integer.parseInt(pp.getAttributeValue(null, "x")); final int y = Integer.parseInt(pp.getAttributeValue(null, "y")); return new Location(LocationType.POI, null, y, x, null, name); } throw new IllegalStateException("cannot handle: " + type); } private final Location parseAddress(final XmlPullParser pp) { final String type = pp.getName(); if ("Address".equals(type)) { String name = pp.getAttributeValue(null, "name").trim(); if (name.equals("unknown")) name = null; final int x = Integer.parseInt(pp.getAttributeValue(null, "x")); final int y = Integer.parseInt(pp.getAttributeValue(null, "y")); final String[] placeAndName = splitPlaceAndName(name); return new Location(LocationType.ADDRESS, null, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private static final Location parseReqLoc(final XmlPullParser pp) { final String type = pp.getName(); if ("ReqLoc".equals(type)) { XmlPullUtil.requireAttr(pp, "type", "ADR"); final String name = pp.getAttributeValue(null, "output").trim(); return new Location(LocationType.ADDRESS, null, null, name); } throw new IllegalStateException("cannot handle: " + type); } private static final Position parsePlatform(final XmlPullParser pp) throws XmlPullParserException, IOException { XmlPullUtil.enter(pp, "Platform"); XmlPullUtil.require(pp, "Text"); final String platformText = XmlPullUtil.text(pp).trim(); XmlPullUtil.exit(pp, "Platform"); if (platformText.length() == 0) return null; else return new Position(platformText); } protected StringBuilder jsonGetStopsParameters(final CharSequence constraint) { final StringBuilder parameters = new StringBuilder(); parameters.append("?getstop=1"); parameters.append("&REQ0JourneyStopsS0A=255"); parameters.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(constraint.toString(), jsonGetStopsEncoding)).append("?"); // parameters.append("&REQ0JourneyStopsB=12"); parameters.append("&js=true"); return parameters; } private static final Pattern P_AJAX_GET_STOPS_JSON = Pattern.compile("SLs\\.sls\\s*=\\s*(.*?);\\s*SLs\\.showSuggestion\\(\\);", Pattern.DOTALL); private static final Pattern P_AJAX_GET_STOPS_ID = Pattern.compile(".*?@L=(\\d+)@.*?"); protected final List jsonGetStops(final String uri) throws IOException { final CharSequence page = ParserUtils.scrape(uri, null, jsonGetStopsEncoding, null); final Matcher mJson = P_AJAX_GET_STOPS_JSON.matcher(page); if (mJson.matches()) { final String json = mJson.group(1); final List results = new ArrayList(); try { final JSONObject head = new JSONObject(json); final JSONArray aSuggestions = head.getJSONArray("suggestions"); for (int i = 0; i < aSuggestions.length(); i++) { final JSONObject suggestion = aSuggestions.optJSONObject(i); if (suggestion != null) { final int type = suggestion.getInt("type"); final String value = suggestion.getString("value"); final int lat = suggestion.optInt("ycoord"); final int lon = suggestion.optInt("xcoord"); String localId = null; final Matcher m = P_AJAX_GET_STOPS_ID.matcher(suggestion.getString("id")); if (m.matches()) localId = m.group(1); if (type == 1) // station { final String[] placeAndName = splitPlaceAndName(value); results.add(new Location(LocationType.STATION, localId, lat, lon, placeAndName[0], placeAndName[1])); } else if (type == 2) // address { final String[] placeAndName = splitPlaceAndName(value); results.add(new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0], placeAndName[1])); } else if (type == 4) // poi { results.add(new Location(LocationType.POI, localId, lat, lon, null, value)); } else if (type == 71) // strange (VBN) { // TODO don't know what to do } else if (type == 87) // strange (ZTM) { // TODO don't know what to do } else if (type == 128) // strange (SEPTA) { // TODO don't know what to do } else { throw new IllegalStateException("unknown type " + type + " on " + uri); } } } return results; } catch (final JSONException x) { x.printStackTrace(); throw new RuntimeException("cannot parse: '" + json + "' on " + uri, x); } } else { throw new RuntimeException("cannot parse: '" + page + "' on " + uri); } } protected final List xmlLocationList(final String uri) throws IOException { Reader reader = null; try { reader = new InputStreamReader(ParserUtils.scrapeInputStream(uri), UTF_8); final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(reader); final List results = new ArrayList(); pp.require(XmlPullParser.START_DOCUMENT, null, null); pp.next(); XmlPullUtil.enter(pp, "LocationList"); while (XmlPullUtil.test(pp, "StopLocation") || XmlPullUtil.test(pp, "CoordLocation")) { final String name = ParserUtils.resolveEntities(XmlPullUtil.attr(pp, "name")); final int lon = XmlPullUtil.intAttr(pp, "x"); final int lat = XmlPullUtil.intAttr(pp, "y"); if (XmlPullUtil.test(pp, "StopLocation")) { final String id = XmlPullUtil.attr(pp, "id"); final String[] placeAndName = splitPlaceAndName(name); results.add(new Location(LocationType.STATION, id, lat, lon, placeAndName[0], placeAndName[1])); } else { final String type = XmlPullUtil.attr(pp, "type"); if ("POI".equals(type)) results.add(new Location(LocationType.POI, null, lat, lon, null, name)); else if ("ADR".equals(type)) results.add(new Location(LocationType.ADDRESS, null, lat, lon, null, name)); else throw new IllegalStateException("unknown type " + type + " on " + uri); } if (pp.isEmptyElementTag()) { XmlPullUtil.next(pp); } else { XmlPullUtil.enter(pp); XmlPullUtil.exit(pp); } } XmlPullUtil.exit(pp, "LocationList"); return results; } catch (final XmlPullParserException x) { throw new RuntimeException(x); } finally { if (reader != null) reader.close(); } } private static final Pattern P_XML_MLC_REQ_ID = Pattern.compile(".*?@L=(\\d+)@.*?"); private static final Pattern P_XML_MLC_REQ_LONLAT = Pattern.compile(".*?@X=(-?\\d+)@Y=(-?\\d+)@.*?"); protected final List xmlMLcReq(final CharSequence constraint) throws IOException { final String mlcReq = ""; final String request = wrapReqC(mlcReq, xmlMlcResEncoding); // ParserUtils.printXml(ParserUtils.scrape(queryEndpoint, request, xmlMlcResEncoding, null)); Reader reader = null; try { reader = new InputStreamReader(ParserUtils.scrapeInputStream(queryEndpoint, request, xmlMlcResEncoding, null, null, 3), xmlMlcResEncoding); final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(reader); final List results = new ArrayList(); XmlPullUtil.enter(pp, "ResC"); XmlPullUtil.enter(pp, "MLcRes"); while (XmlPullUtil.test(pp, "MLc")) { final String t = XmlPullUtil.attr(pp, "t"); final LocationType type; if ("ST".equals(t)) type = LocationType.STATION; else if ("POI".equals(t)) type = LocationType.POI; else if ("ADR".equals(t)) type = LocationType.ADDRESS; else throw new IllegalStateException("cannot handle: '" + t + "'"); final String id; final String i = pp.getAttributeValue(null, "i"); if (i != null) { final Matcher iMatcherId = P_XML_MLC_REQ_ID.matcher(i); if (!iMatcherId.matches()) throw new IllegalStateException("cannot parse id: '" + i + "'"); id = iMatcherId.group(1); } else { id = null; } final String name = XmlPullUtil.attr(pp, "n"); final String[] placeAndName = splitPlaceAndName(name); final String r = pp.getAttributeValue(null, "r"); final Matcher iMatcherLonLat = P_XML_MLC_REQ_LONLAT.matcher(i != null ? i : r); final int lat; final int lon; if (iMatcherLonLat.matches()) { lon = Integer.parseInt(iMatcherLonLat.group(1)); lat = Integer.parseInt(iMatcherLonLat.group(2)); } else { lat = 0; lon = 0; } final Location location = new Location(type, id, lat, lon, placeAndName[0], placeAndName[1]); if (location.hasLocation()) results.add(location); XmlPullUtil.next(pp); } XmlPullUtil.exit(pp, "MLcRes"); XmlPullUtil.exit(pp, "ResC"); return results; } catch (final XmlPullParserException x) { throw new RuntimeException(x); } finally { if (reader != null) reader.close(); } } protected StringBuilder xmlQueryDeparturesParameters(final String stationId) { final StringBuilder parameters = new StringBuilder(); parameters.append("?productsFilter=").append(allProductsString()); parameters.append("&boardType=dep"); if (canDoEquivs) parameters.append("&disableEquivs=yes"); // don't use nearby stations parameters.append("&maxJourneys=50"); // ignore maxDepartures because result contains other stations parameters.append("&start=yes"); parameters.append("&L=vs_java3"); parameters.append("&input=").append(stationId); if (clientType != null) parameters.append("&clientType=").append(ParserUtils.urlEncode(clientType)); return parameters; } private static final Pattern P_XML_QUERY_DEPARTURES_DELAY = Pattern.compile("(?:-|k\\.A\\.?|cancel|\\+?\\s*(\\d+))"); protected QueryDeparturesResult xmlQueryDepartures(final String uri, final String stationId) throws IOException { StringReplaceReader reader = null; try { // work around unparsable XML reader = new StringReplaceReader(new InputStreamReader(ParserUtils.scrapeInputStream(uri), ISO_8859_1), " & ", " & "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("
", " "); reader.replace(" ->", " →"); // right arrow reader.replace(" <-", " ←"); // left arrow reader.replace(" <> ", " ↔ "); // left-right arrow addCustomReplaces(reader); // System.out.println(uri); // ParserUtils.printFromReader(reader); final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(reader); pp.nextTag(); final ResultHeader header = new ResultHeader(SERVER_PRODUCT); final QueryDeparturesResult result = new QueryDeparturesResult(header); final List departures = new ArrayList(8); if (XmlPullUtil.test(pp, "Err")) { final String code = XmlPullUtil.attr(pp, "code"); final String text = XmlPullUtil.attr(pp, "text"); if (code.equals("H730")) // Your input is not valid return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION); if (code.equals("H890")) { result.stationDepartures.add(new StationDepartures(new Location(LocationType.STATION, stationId), Collections . emptyList(), null)); return result; } throw new IllegalArgumentException("unknown error " + code + ", " + text); } if (XmlPullUtil.test(pp, "StationTable")) { XmlPullUtil.enter(pp, "StationTable"); } while (XmlPullUtil.test(pp, "Journey")) { final String fpTime = XmlPullUtil.attr(pp, "fpTime"); final String fpDate = XmlPullUtil.attr(pp, "fpDate"); final String delay = XmlPullUtil.attr(pp, "delay"); final String eDelay = pp.getAttributeValue(null, "e_delay"); final String platform = pp.getAttributeValue(null, "platform"); // TODO newpl final String targetLoc = pp.getAttributeValue(null, "targetLoc"); // TODO hafasname final String dirnr = pp.getAttributeValue(null, "dirnr"); final String prod = XmlPullUtil.attr(pp, "prod"); final String classStr = pp.getAttributeValue(null, "class"); final String dir = pp.getAttributeValue(null, "dir"); final String capacityStr = pp.getAttributeValue(null, "capacity"); final String depStation = pp.getAttributeValue(null, "depStation"); final String delayReason = pp.getAttributeValue(null, "delayReason"); // TODO is_reachable // TODO disableTrainInfo final boolean isEquivStation = canDoEquivs && depStation != null; if (!isEquivStation && !"cancel".equals(eDelay)) { final Calendar plannedTime = new GregorianCalendar(timeZone()); plannedTime.clear(); ParserUtils.parseEuropeanTime(plannedTime, fpTime); if (fpDate.length() == 8) ParserUtils.parseGermanDate(plannedTime, fpDate); else if (fpDate.length() == 10) ParserUtils.parseIsoDate(plannedTime, fpDate); else throw new IllegalStateException("cannot parse: '" + fpDate + "'"); final Calendar predictedTime; if (eDelay != null) { predictedTime = new GregorianCalendar(timeZone()); predictedTime.setTimeInMillis(plannedTime.getTimeInMillis()); predictedTime.add(Calendar.MINUTE, Integer.parseInt(eDelay)); } else if (delay != null) { final Matcher m = P_XML_QUERY_DEPARTURES_DELAY.matcher(delay); if (m.matches()) { if (m.group(1) != null) { predictedTime = new GregorianCalendar(timeZone()); predictedTime.setTimeInMillis(plannedTime.getTimeInMillis()); predictedTime.add(Calendar.MINUTE, Integer.parseInt(m.group(1))); } else { predictedTime = null; } } else { throw new RuntimeException("cannot parse delay: '" + delay + "'"); } } else { predictedTime = null; } final Position position = platform != null ? new Position("Gl. " + ParserUtils.resolveEntities(platform)) : null; final String[] destinationPlaceAndName; if (dir != null) destinationPlaceAndName = splitPlaceAndName(dir.trim()); else if (targetLoc != null) destinationPlaceAndName = splitPlaceAndName(targetLoc.trim()); else destinationPlaceAndName = null; final String destinationId; if (dirnr != null) destinationId = dirnr; else destinationId = null; final Location destination = new Location(destinationId != null ? LocationType.STATION : LocationType.ANY, destinationId, destinationPlaceAndName != null ? destinationPlaceAndName[0] : null, destinationPlaceAndName != null ? destinationPlaceAndName[1] : null); final Line prodLine = parseLineAndType(prod); final Line line; if (classStr != null) { final char classChar = intToProduct(Integer.parseInt(classStr)); if (classChar == 0) throw new IllegalArgumentException(); // could check for type consistency here final String lineName = prodLine.label.substring(1); if (prodLine.attrs != null) line = newLine(classChar, lineName, null, prodLine.attrs.toArray(new Line.Attr[0])); else line = newLine(classChar, lineName, null); } else { line = prodLine; } final int[] capacity; if (capacityStr != null && !"0|0".equals(capacityStr)) { final String[] capacityParts = capacityStr.split("\\|"); capacity = new int[] { Integer.parseInt(capacityParts[0]), Integer.parseInt(capacityParts[1]) }; } else { capacity = null; } final String message; if (delayReason != null) { final String msg = delayReason.trim(); message = msg.length() > 0 ? msg : null; } else { message = null; } final Departure departure = new Departure(plannedTime.getTime(), predictedTime != null ? predictedTime.getTime() : null, line, position, destination, capacity, message); departures.add(departure); } XmlPullUtil.requireSkip(pp, "Journey"); } result.stationDepartures.add(new StationDepartures(new Location(LocationType.STATION, stationId), departures, null)); return result; } catch (final XmlPullParserException x) { throw new RuntimeException(x); } finally { if (reader != null) reader.close(); } } protected void addCustomReplaces(final StringReplaceReader reader) { } public QueryTripsResult queryTrips(final Location from, final Location via, final Location to, final Date date, final boolean dep, final Collection products, final WalkSpeed walkSpeed, final Accessibility accessibility, final Set