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