/*
* Copyright 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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import de.schildbach.pte.dto.Departure;
import de.schildbach.pte.dto.Fare;
import de.schildbach.pte.dto.Line;
import de.schildbach.pte.dto.Location;
import de.schildbach.pte.dto.LocationType;
import de.schildbach.pte.dto.NearbyLocationsResult;
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.Style;
import de.schildbach.pte.dto.SuggestLocationsResult;
import de.schildbach.pte.dto.SuggestedLocation;
import de.schildbach.pte.dto.Trip;
import de.schildbach.pte.dto.TripOptions;
import de.schildbach.pte.exception.ParserException;
import de.schildbach.pte.util.ParserUtils;
import de.schildbach.pte.util.PolylineFormat;
import okhttp3.HttpUrl;
/**
* This is an implementation of the HCI (HAFAS Client Interface).
*
* @author Andreas Schildbach
*/
public abstract class AbstractHafasClientInterfaceProvider extends AbstractHafasProvider {
private final HttpUrl apiBase;
private String apiEndpoint = "mgate.exe";
@Nullable
private String apiVersion;
@Nullable
private String apiExt;
@Nullable
private String apiAuthorization;
@Nullable
private String apiClient;
@Nullable
private byte[] requestChecksumSalt;
@Nullable
private byte[] requestMicMacSalt;
private static final String SERVER_PRODUCT = "hci";
@SuppressWarnings("deprecation")
private static final HashFunction MD5 = Hashing.md5();
private static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
public AbstractHafasClientInterfaceProvider(final NetworkId network, final HttpUrl apiBase,
final Product[] productsMap) {
super(network, productsMap);
this.apiBase = checkNotNull(apiBase);
}
public HttpUrl getApiBase() {
return apiBase;
}
protected AbstractHafasClientInterfaceProvider setApiEndpoint(final String apiEndpoint) {
this.apiEndpoint = checkNotNull(apiEndpoint);
return this;
}
public String getApiEndpoint() {
return apiEndpoint;
}
protected AbstractHafasClientInterfaceProvider setApiVersion(final String apiVersion) {
checkArgument(apiVersion.compareToIgnoreCase("1.14") >= 0, "apiVersion must be 1.14 or higher");
this.apiVersion = apiVersion;
return this;
}
public String getApiVersion() {
return apiVersion;
}
protected AbstractHafasClientInterfaceProvider setApiExt(final String apiExt) {
this.apiExt = checkNotNull(apiExt);
return this;
}
public String getApiExt() {
return apiExt;
}
protected AbstractHafasClientInterfaceProvider setApiAuthorization(final String apiAuthorization) {
this.apiAuthorization = apiAuthorization;
return this;
}
public String getApiAuthorization() {
return apiAuthorization;
}
protected AbstractHafasClientInterfaceProvider setApiClient(final String apiClient) {
this.apiClient = apiClient;
return this;
}
public String getApiClient() {
return apiClient;
}
protected AbstractHafasClientInterfaceProvider setRequestChecksumSalt(final byte[] requestChecksumSalt) {
this.requestChecksumSalt = requestChecksumSalt;
return this;
}
public byte[] getRequestChecksumSalt() {
return requestChecksumSalt;
}
protected AbstractHafasClientInterfaceProvider setRequestMicMacSalt(final byte[] requestMicMacSalt) {
this.requestMicMacSalt = requestMicMacSalt;
return this;
}
public byte[] getRequestMicMacSalt() {
return requestMicMacSalt;
}
@Override
public NearbyLocationsResult queryNearbyLocations(final Set types, final Location location,
final int maxDistance, final int maxLocations) throws IOException {
if (location.hasCoord())
return jsonLocGeoPos(types, location.coord, maxDistance, maxLocations);
else
throw new IllegalArgumentException("cannot handle: " + location);
}
@Override
public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time,
final int maxDepartures, final boolean equivs) throws IOException {
return jsonStationBoard(stationId, time, maxDepartures, equivs);
}
@Override
public SuggestLocationsResult suggestLocations(final CharSequence constraint,
final @Nullable Set types, final int maxLocations) throws IOException {
return jsonLocMatch(constraint, types, maxLocations);
}
@Override
public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to,
final Date date, final boolean dep, final @Nullable TripOptions options) throws IOException {
return jsonTripSearch(from, via, to, date, dep, options != null ? options.products : null,
options != null ? options.walkSpeed : null, null);
}
@Override
public QueryTripsResult queryMoreTrips(final QueryTripsContext context, final boolean later) throws IOException {
final JsonContext jsonContext = (JsonContext) context;
return jsonTripSearch(jsonContext.from, jsonContext.via, jsonContext.to, jsonContext.date, jsonContext.dep,
jsonContext.products, jsonContext.walkSpeed, later ? jsonContext.laterContext : jsonContext.earlierContext);
}
protected final NearbyLocationsResult jsonLocGeoPos(final Set types, final Point coord,
int maxDistance, int maxLocations) throws IOException {
if (maxDistance == 0)
maxDistance = DEFAULT_MAX_DISTANCE;
if (maxLocations == 0)
maxLocations = DEFAULT_MAX_LOCATIONS;
final boolean getStations = types.contains(LocationType.STATION);
final boolean getPOIs = types.contains(LocationType.POI);
final String request = wrapJsonApiRequest("LocGeoPos", "{\"ring\":" //
+ "{\"cCrd\":{\"x\":" + coord.getLonAs1E6() + ",\"y\":" + coord.getLatAs1E6() + "}," //
+ "\"maxDist\":" + maxDistance + "}," //
+ "\"getStops\":" + getStations + "," //
+ "\"getPOIs\":" + getPOIs + "," //
+ "\"maxLoc\":" + maxLocations + "}", //
false);
final HttpUrl url = requestUrl(request);
final CharSequence page = httpClient.get(url, request, "application/json");
try {
final JSONObject head = new JSONObject(page.toString());
final String headErr = head.optString("err", null);
if (headErr != null && !"OK".equals(headErr)) {
final String headErrTxt = head.optString("errTxt");
throw new RuntimeException(headErr + " " + headErrTxt);
}
final JSONArray svcResList = head.getJSONArray("svcResL");
checkState(svcResList.length() == 2);
final ResultHeader header = parseServerInfo(svcResList.getJSONObject(0), head.getString("ver"));
final JSONObject svcRes = svcResList.getJSONObject(1);
checkState("LocGeoPos".equals(svcRes.getString("meth")));
final String err = svcRes.getString("err");
if (!"OK".equals(err)) {
final String errTxt = svcRes.optString("errTxt");
log.debug("Hafas error: {} {}", err, errTxt);
if ("FAIL".equals(err) && "HCI Service: request failed".equals(errTxt))
return new NearbyLocationsResult(header, NearbyLocationsResult.Status.SERVICE_DOWN);
if ("CGI_READ_FAILED".equals(err))
return new NearbyLocationsResult(header, NearbyLocationsResult.Status.SERVICE_DOWN);
if ("CGI_NO_SERVER".equals(err))
return new NearbyLocationsResult(header, NearbyLocationsResult.Status.SERVICE_DOWN);
throw new RuntimeException(err + " " + errTxt);
}
final JSONObject res = svcRes.getJSONObject("res");
final JSONObject common = res.getJSONObject("common");
/* final List remarks = */ parseRemList(common.getJSONArray("remL"));
final JSONArray crdSysList = common.optJSONArray("crdSysL");
final JSONArray locL = res.optJSONArray("locL");
final List locations;
if (locL != null) {
locations = parseLocList(locL, crdSysList);
// filter unwanted location types
for (Iterator i = locations.iterator(); i.hasNext();) {
final Location location = i.next();
if (!types.contains(location.type))
i.remove();
}
} else {
locations = Collections.emptyList();
}
return new NearbyLocationsResult(header, locations);
} catch (final JSONException x) {
throw new ParserException("cannot parse json: '" + page + "' on " + url, x);
}
}
protected final QueryDeparturesResult jsonStationBoard(final String stationId, final @Nullable Date time,
final int maxDepartures, final boolean equivs) throws IOException {
final Calendar c = new GregorianCalendar(timeZone);
c.setTime(time);
final CharSequence jsonDate = jsonDate(c);
final CharSequence jsonTime = jsonTime(c);
final CharSequence normalizedStationId = normalizeStationId(stationId);
final CharSequence stbFltrEquiv = Boolean.toString(!equivs);
final CharSequence maxJny = Integer.toString(maxDepartures != 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES);
final String request = wrapJsonApiRequest("StationBoard", "{\"type\":\"DEP\"," //
+ "\"date\":\"" + jsonDate + "\"," //
+ "\"time\":\"" + jsonTime + "\"," //
+ "\"stbLoc\":{\"type\":\"S\"," + "\"state\":\"F\"," // F/M
+ "\"extId\":" + JSONObject.quote(normalizedStationId.toString()) + "}," //
+ (apiVersion.compareToIgnoreCase("1.19") < 0 ? "\"stbFltrEquiv\":" + stbFltrEquiv + "," : "") //
+ "\"maxJny\":" + maxJny + "}", false);
final HttpUrl url = requestUrl(request);
final CharSequence page = httpClient.get(url, request, "application/json");
try {
final JSONObject head = new JSONObject(page.toString());
final String headErr = head.optString("err", null);
if (headErr != null && !"OK".equals(headErr)) {
final String headErrTxt = head.optString("errTxt");
throw new RuntimeException(headErr + " " + headErrTxt);
}
final JSONArray svcResList = head.getJSONArray("svcResL");
checkState(svcResList.length() == 2);
final ResultHeader header = parseServerInfo(svcResList.getJSONObject(0), head.getString("ver"));
final QueryDeparturesResult result = new QueryDeparturesResult(header);
final JSONObject svcRes = svcResList.optJSONObject(1);
checkState("StationBoard".equals(svcRes.getString("meth")));
final String err = svcRes.getString("err");
if (!"OK".equals(err)) {
final String errTxt = svcRes.optString("errTxt");
log.debug("Hafas error: {} {}", err, errTxt);
if ("LOCATION".equals(err) && "HCI Service: location missing or invalid".equals(errTxt))
return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION);
if ("FAIL".equals(err) && "HCI Service: request failed".equals(errTxt))
return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
if ("PROBLEMS".equals(err) && "HCI Service: problems during service execution".equals(errTxt))
return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
if ("CGI_READ_FAILED".equals(err))
return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
if ("CGI_NO_SERVER".equals(err))
return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
throw new RuntimeException(err + " " + errTxt);
}
final JSONObject res = svcRes.getJSONObject("res");
final JSONObject common = res.getJSONObject("common");
final List remarks = parseRemList(common.getJSONArray("remL"));
final List