/* * 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.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.SuggestLocationsResult; import de.schildbach.pte.dto.SuggestedLocation; import de.schildbach.pte.dto.Trip; import de.schildbach.pte.exception.ParserException; 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 static final int DEFAULT_MAX_DEPARTURES = 100; 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 boolean dominantPlanStopTime = false; private boolean useIso8601 = false; private String extXmlEndpoint = null; private boolean stationBoardHasStationTable = true; private boolean stationBoardHasLocation = false; private boolean stationBoardCanDoEquivs = true; 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); } public AbstractHafasProvider(final String stationBoardEndpoint, final String getStopEndpoint, final String queryEndpoint, final int numProductBits, final Charset jsonEncoding) { this.stationBoardEndpoint = stationBoardEndpoint; this.getStopEndpoint = getStopEndpoint; this.queryEndpoint = queryEndpoint; this.numProductBits = numProductBits; this.jsonGetStopsEncoding = jsonEncoding; this.jsonNearbyStationsEncoding = jsonEncoding; } 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 setUseIso8601(final boolean useIso8601) { this.useIso8601 = useIso8601; } protected void setExtXmlEndpoint(final String extXmlEndpoint) { this.extXmlEndpoint = extXmlEndpoint; } protected void setStationBoardHasStationTable(final boolean stationBoardHasStationTable) { this.stationBoardHasStationTable = stationBoardHasStationTable; } protected void setStationBoardHasLocation(final boolean stationBoardHasLocation) { this.stationBoardHasLocation = stationBoardHasLocation; } protected void setStationBoardCanDoEquivs(final boolean canDoEquivs) { this.stationBoardCanDoEquivs = canDoEquivs; } 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"); final String platformText = XmlPullUtil.valueTag(pp, "Text"); XmlPullUtil.skipExit(pp, "Platform"); if (platformText == null || platformText.length() == 0) return null; else return new Position(platformText); } public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException { final StringBuilder uri = new StringBuilder(getStopEndpoint); appendJsonGetStopsParameters(uri, constraint, 0); return jsonGetStops(uri.toString()); } protected void appendJsonGetStopsParameters(final StringBuilder uri, final CharSequence constraint, final int maxStops) { uri.append("?getstop=1"); uri.append("&REQ0JourneyStopsS0A=255"); uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(constraint.toString(), jsonGetStopsEncoding)).append("?"); if (maxStops > 0) uri.append("&REQ0JourneyStopsB=").append(maxStops); uri.append("&js=true"); } 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=0*(\\d+)@.*?"); protected final SuggestLocationsResult 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 locations = 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"); final int weight = suggestion.getInt("weight"); 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); final Location location = new Location(LocationType.STATION, localId, lat, lon, placeAndName[0], placeAndName[1]); locations.add(new SuggestedLocation(location, weight)); } else if (type == 2) // address { final String[] placeAndName = splitPlaceAndName(value); final Location location = new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0], placeAndName[1]); locations.add(new SuggestedLocation(location, weight)); } else if (type == 4) // poi { final Location location = new Location(LocationType.POI, localId, lat, lon, null, value); locations.add(new SuggestedLocation(location, weight)); } 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 new SuggestLocationsResult(new ResultHeader(SERVER_PRODUCT), locations); } catch (final JSONException x) { x.printStackTrace(); throw new RuntimeException("cannot parse: '" + json + "' on " + uri, x); } } else { throw new RuntimeException("cannot parse: '" + page + "' on " + uri); } } public QueryDeparturesResult queryDepartures(final String stationId, final int maxDepartures, final boolean equivs) throws IOException { final StringBuilder uri = new StringBuilder(stationBoardEndpoint); appendXmlStationBoardParameters(uri, stationId, maxDepartures); return xmlStationBoard(uri.toString(), stationId); } protected void appendXmlStationBoardParameters(final StringBuilder uri, final String stationId, final int maxDepartures) { uri.append("?productsFilter=").append(allProductsString()); uri.append("&boardType=dep"); if (stationBoardCanDoEquivs) uri.append("&disableEquivs=yes"); // don't use nearby stations uri.append("&maxJourneys=").append(maxDepartures > 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES); uri.append("&start=yes"); uri.append("&L=vs_java3"); uri.append("&input=").append(normalizeStationId(stationId)); if (clientType != null) uri.append("&clientType=").append(ParserUtils.urlEncode(clientType)); } private static final Pattern P_XML_STATION_BOARD_DELAY = Pattern.compile("(?:-|k\\.A\\.?|cancel|\\+?\\s*(\\d+))"); protected final QueryDeparturesResult xmlStationBoard(final String uri, final String stationId) throws IOException { final String normalizedStationId = normalizeStationId(stationId); 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, normalizedStationId), Collections . emptyList(), null)); return result; } throw new IllegalArgumentException("unknown error " + code + ", " + text); } String[] stationPlaceAndName = null; if (stationBoardHasStationTable) XmlPullUtil.enter(pp, "StationTable"); if (stationBoardHasLocation) { XmlPullUtil.require(pp, "St"); final String evaId = XmlPullUtil.attr(pp, "evaId"); if (evaId != null) { if (!evaId.equals(normalizedStationId)) throw new IllegalStateException("stationId: " + normalizedStationId + ", evaId: " + evaId); final String name = XmlPullUtil.attr(pp, "name"); if (name != null) stationPlaceAndName = splitPlaceAndName(name.trim()); } XmlPullUtil.requireSkip(pp, "St"); } 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 = stationBoardCanDoEquivs && 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_STATION_BOARD_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"); } if (stationBoardHasStationTable) XmlPullUtil.exit(pp, "StationTable"); XmlPullUtil.requireEndDocument(pp); result.stationDepartures.add(new StationDepartures(new Location(LocationType.STATION, normalizedStationId, stationPlaceAndName != null ? stationPlaceAndName[0] : null, stationPlaceAndName != null ? stationPlaceAndName[1] : null), 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