/*
* 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