/*
* Copyright 2010-2012 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.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.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.Connection;
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.QueryConnectionsContext;
import de.schildbach.pte.dto.QueryConnectionsResult;
import de.schildbach.pte.dto.QueryDeparturesResult;
import de.schildbach.pte.dto.ResultHeader;
import de.schildbach.pte.dto.StationDepartures;
import de.schildbach.pte.dto.Stop;
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 PROD = "hafas";
private final String apiUri;
private final int numProductBits;
private final String accessId;
private final Charset jsonEncoding;
private final Charset xmlMlcResEncoding;
private boolean dominantPlanStopTime = false;
private static class Context implements QueryConnectionsContext
{
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 QueryConnectionsBinaryContext implements QueryConnectionsContext
{
public final String ident;
public final int seqNr;
public final String ld;
public QueryConnectionsBinaryContext(final String ident, final int seqNr, final String ld)
{
this.ident = ident;
this.seqNr = seqNr;
this.ld = ld;
}
public boolean canQueryLater()
{
return true;
}
public boolean canQueryEarlier()
{
return true;
}
}
public AbstractHafasProvider(final String apiUri, final int numProductBits, final String accessId, final Charset jsonEncoding,
final Charset xmlMlcResEncoding)
{
this.apiUri = apiUri;
this.numProductBits = numProductBits;
this.accessId = accessId;
this.jsonEncoding = jsonEncoding;
this.xmlMlcResEncoding = xmlMlcResEncoding;
}
public AbstractHafasProvider(final String apiUri, final int numProductBits, final String accessId)
{
this.apiUri = apiUri;
this.numProductBits = numProductBits;
this.accessId = accessId;
this.jsonEncoding = ISO_8859_1;
this.xmlMlcResEncoding = ISO_8859_1;
}
protected void setDominantPlanStopTime(final boolean dominantPlanStopTime)
{
this.dominantPlanStopTime = dominantPlanStopTime;
}
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, char 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 wrap(final String 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 int id = Integer.parseInt(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, 0, 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, 0, 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, 0, null, name);
}
throw new IllegalStateException("cannot handle: " + type);
}
private static final String parsePlatform(final XmlPullParser pp) throws XmlPullParserException, IOException
{
XmlPullUtil.enter(pp, "Platform");
XmlPullUtil.require(pp, "Text");
final String position = XmlPullUtil.text(pp).trim();
XmlPullUtil.exit(pp, "Platform");
if (position.length() == 0)
return null;
else
return position;
}
public List xmlLocValReq(final CharSequence constraint) throws IOException
{
final String request = "";
// System.out.println(ParserUtils.scrape(apiUri, true, wrap(request), null, false));
Reader reader = null;
try
{
reader = new InputStreamReader(ParserUtils.scrapeInputStream(apiUri, wrap(request, null), null, null, null, 3), ISO_8859_1);
final List results = new ArrayList();
final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
final XmlPullParser pp = factory.newPullParser();
pp.setInput(reader);
assertResC(pp);
XmlPullUtil.enter(pp);
XmlPullUtil.require(pp, "LocValRes");
XmlPullUtil.requireAttr(pp, "id", "req");
XmlPullUtil.enter(pp);
while (pp.getEventType() == XmlPullParser.START_TAG)
{
final String tag = pp.getName();
if ("Station".equals(tag))
results.add(parseStation(pp));
else if ("Poi".equals(tag))
results.add(parsePoi(pp));
else if ("Address".equals(tag))
results.add(parseAddress(pp));
else if ("ReqLoc".equals(tag))
/* results.add(parseReqLoc(pp)) */;
else
System.out.println("cannot handle tag: " + tag);
XmlPullUtil.next(pp);
}
XmlPullUtil.exit(pp);
return results;
}
catch (final XmlPullParserException x)
{
throw new RuntimeException(x);
}
finally
{
if (reader != null)
reader.close();
}
}
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, jsonEncoding, 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");
int localId = 0;
final Matcher m = P_AJAX_GET_STOPS_ID.matcher(suggestion.getString("id"));
if (m.matches())
localId = Integer.parseInt(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, 0, 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");
if (pp.isWhitespace())
pp.next();
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 int id = XmlPullUtil.intAttr(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, 0, lat, lon, null, name));
else if ("ADR".equals(type))
results.add(new Location(LocationType.ADDRESS, 0, 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);
}
if (pp.isWhitespace())
pp.next();
}
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 request = "";
final String wrappedRequest = wrap(request, xmlMlcResEncoding);
// ParserUtils.printXml(ParserUtils.scrape(apiUri, true, wrappedRequest, xmlMlcResEncoding, null));
Reader reader = null;
try
{
reader = new InputStreamReader(ParserUtils.scrapeInputStream(apiUri, wrappedRequest, 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();
assertResC(pp);
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 int 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 = Integer.parseInt(iMatcherId.group(1));
}
else
{
id = 0;
}
final String name = XmlPullUtil.attr(pp, "n");
final String r = pp.getAttributeValue(null, "r");
final Matcher iMatcherLonLat = P_XML_MLC_REQ_LONLAT.matcher(i != null ? i : r);
if (!iMatcherLonLat.matches())
throw new IllegalStateException("cannot parse lon/lat: '" + i + "' or '" + r + "'");
final int lon = Integer.parseInt(iMatcherLonLat.group(1));
final int lat = Integer.parseInt(iMatcherLonLat.group(2));
final String[] placeAndName = splitPlaceAndName(name);
results.add(new Location(type, id, lat, lon, placeAndName[0], placeAndName[1]));
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();
}
}
private static final Pattern P_XML_QUERY_DEPARTURES_DELAY = Pattern.compile("(?:-|k\\.A\\.?|cancel|\\+?\\s*(\\d+))");
protected QueryDeparturesResult xmlQueryDepartures(final String uri, final int stationId) throws IOException
{
StringReplaceReader reader = null;
try
{
// work around unparsable XML
reader = new StringReplaceReader(new InputStreamReader(ParserUtils.scrapeInputStream(uri), ISO_8859_1), " & ", " & ");
reader.replace("Ringbahn ->", "Ringbahn ->"); // Berlin
reader.replace("Ringbahn <-", "Ringbahn <-"); // Berlin
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");
if (pp.getEventType() == XmlPullParser.TEXT)
pp.nextTag();
}
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
if (depStation == null && !"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 String position = platform != null ? "Gl. " + ParserUtils.resolveEntities(platform) : null;
final String destinationName;
if (dir != null)
destinationName = dir.trim();
else if (targetLoc != null)
destinationName = targetLoc.trim();
else
destinationName = null;
final int destinationId;
if (dirnr != null)
destinationId = Integer.parseInt(dirnr);
else
destinationId = 0;
final Location destination = new Location(destinationId > 0 ? LocationType.STATION : LocationType.ANY, destinationId, null,
destinationName);
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, prodLine.attrs.toArray(new Line.Attr[0]));
else
line = newLine(classChar, lineName);
}
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);
}
if (pp.isEmptyElementTag())
{
XmlPullUtil.next(pp);
}
else
{
XmlPullUtil.enter(pp, "Journey");
XmlPullUtil.exit(pp, "Journey");
}
if (pp.getEventType() == XmlPullParser.TEXT)
pp.nextTag();
}
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 QueryConnectionsResult queryConnections(final Location from, final Location via, final Location to, final Date date, final boolean dep,
final int numConnections, final String products, final WalkSpeed walkSpeed, final Accessibility accessibility, final Set