package cgeo.geocaching.connector.trackable;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.R;
import cgeo.geocaching.enumerations.StatusCode;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.log.AbstractLoggingActivity;
import cgeo.geocaching.log.LogTypeTrackable;
import cgeo.geocaching.log.TrackableLog;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.models.Trackable;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Parameters;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.storage.DataStore;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.Version;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.xml.sax.InputSource;
public class GeokretyConnector extends AbstractTrackableConnector {
/*
1) tracking code:
is generated from the alphabet:
"a b c d e f g h i j k l m n p q r s t u v w x y z 1 2 3 4 5 6 7 8 9"
(no O and 0)
sanity-check for tracking code: if generated code look like reference
number (ie GKxxxx):
preg_match("#^gk[0-9a-f]{4}$#i", $tc)
2) reference number (GKxxxx):
it is just a subsequent number in the database ($id) converted to hex:
$gk=sprintf("GK%04X",$id);
$id=hexdec(substr($gk, 2, 4));
*/
private static final Pattern PATTERN_GK_CODE = Pattern.compile("GK[0-9A-F]{4,}");
private static final Pattern PATTERN_GK_CODE_EXTENDED = Pattern.compile("(GK[0-9A-F]{4,})|([1-9A-NP-Z]{6})");
private static final String HOST = "geokrety.org";
public static final String URL = "https://" + HOST;
private static final String URLPROXY = "https://api.geokretymap.org";
@Override
@NonNull
public String getHost() {
return HOST;
}
@Override
@NonNull
public String getHostUrl() {
return URL;
}
@Override
@Nullable
public String getProxyUrl() {
return URLPROXY;
}
@Override
public int getPreferenceActivity() {
return R.string.preference_screen_geokrety;
}
@Override
public boolean canHandleTrackable(@Nullable final String geocode) {
return geocode != null && PATTERN_GK_CODE.matcher(geocode).matches();
}
@Override
public boolean canHandleTrackable(@Nullable final String geocode, @Nullable final TrackableBrand brand) {
if (brand != TrackableBrand.GEOKRETY) {
return canHandleTrackable(geocode);
}
return geocode != null && PATTERN_GK_CODE_EXTENDED.matcher(geocode).matches();
}
@Override
@NonNull
public String getServiceTitle() {
return CgeoApplication.getInstance().getString(R.string.init_geokrety);
}
@Override
@NonNull
public String getUrl(@NonNull final Trackable trackable) {
return URL + "/konkret.php?id=" + getId(trackable.getGeocode());
}
@Override
@Nullable
public Trackable searchTrackable(final String geocode, final String guid, final String id) {
return searchTrackable(geocode);
}
private static String getUrlCache() {
return Settings.isGeokretyCacheActive() ? URLPROXY : URL;
}
@Nullable
public static Trackable searchTrackable(final String geocode) {
final Integer gkid;
if (StringUtils.startsWithIgnoreCase(geocode, "GK")) {
gkid = getId(geocode);
} else {
// This probably a Tracking Code
Log.d("GeokretyConnector.searchTrackable: geocode=" + geocode);
final String geocodeFound = getGeocodeFromTrackingCode(geocode);
if (geocodeFound == null) {
Log.d("GeokretyConnector.searchTrackable: Unable to retrieve trackable by TrackingCode");
return null;
}
gkid = getId(geocodeFound);
}
Log.d("GeokretyConnector.searchTrackable: gkid=" + gkid);
try {
final String urlDetails = Settings.isGeokretyCacheActive() ? URLPROXY + "/export-details.php" : URL + "/export2.php";
final InputStream response = Network.getResponseStream(Network.getRequest(urlDetails + "?gkid=" + gkid));
if (response == null) {
Log.d("GeokretyConnector.searchTrackable: No data from server");
return null;
}
try {
final InputSource is = new InputSource(response);
final List<Trackable> trackables = GeokretyParser.parse(is);
if (CollectionUtils.isNotEmpty(trackables)) {
final Trackable trackable = trackables.get(0);
DataStore.saveTrackable(trackable);
return trackable;
}
} finally {
IOUtils.closeQuietly(response);
}
} catch (final Exception e) {
Log.w("GeokretyConnector.searchTrackable", e);
}
// TODO maybe a fallback to no proxy would be cool?
return null;
}
@Override
@NonNull
public List<Trackable> searchTrackables(final String geocode) {
Log.d("GeokretyConnector.searchTrackables: wpt=" + geocode);
try {
final InputStream response = Network.getResponseStream(Network.getRequest(getUrlCache() + "/export2.php?wpt=" + URLEncoder.encode(geocode, "utf-8")));
if (response == null) {
Log.d("GeokretyConnector.searchTrackable: No data from server");
return Collections.emptyList();
}
try {
final InputSource is = new InputSource(response);
return GeokretyParser.parse(is);
} finally {
IOUtils.closeQuietly(response);
}
} catch (final Exception e) {
Log.w("GeokretyConnector.searchTrackables", e);
return Collections.emptyList();
}
}
@Override
@NonNull
public List<Trackable> loadInventory() {
return loadInventory(0);
}
@NonNull
private static List<Trackable> loadInventory(final int userid) {
Log.d("GeokretyConnector.loadInventory: userid=" + userid);
try {
final Parameters params = new Parameters("inventory", "1");
if (userid > 0) {
// retrieve someone inventory
params.put("userid", String.valueOf(userid));
} else {
if (StringUtils.isBlank(Settings.getGeokretySecId())) {
return Collections.emptyList();
}
// Retrieve inventory, with tracking codes
params.put("secid", Settings.getGeokretySecId());
}
final InputStream response = Network.getResponseStream(Network.getRequest(URL + "/export2.php", params));
if (response == null) {
Log.d("GeokretyConnector.loadInventory: No data from server");
return Collections.emptyList();
}
try {
final InputSource is = new InputSource(response);
return GeokretyParser.parse(is);
} finally {
IOUtils.closeQuietly(response);
}
} catch (final Exception e) {
Log.w("GeokretyConnector.loadInventory", e);
return Collections.emptyList();
}
}
@Override
@NonNull
public Observable<TrackableLog> trackableLogInventory() {
return Observable.fromIterable(loadInventory()).map(new TrackableLogFunction());
}
private static class TrackableLogFunction implements Function<Trackable, TrackableLog> {
@Override
public TrackableLog apply(final Trackable trackable) {
return new TrackableLog(
trackable.getGeocode(),
trackable.getTrackingcode(),
trackable.getName(),
getId(trackable.getGeocode()),
0,
trackable.getBrand()
);
}
}
public static int getId(final String geocode) {
try {
final String hex = geocode.substring(2);
return Integer.parseInt(hex, 16);
} catch (final NumberFormatException e) {
Log.e("Trackable.getId", e);
}
return -1;
}
@Override
@Nullable
public String getTrackableCodeFromUrl(@NonNull final String url) {
// http://geokrety.org/konkret.php?id=38545
final String gkId = StringUtils.substringAfterLast(url, "konkret.php?id=");
if (StringUtils.isNumeric(gkId)) {
return geocode(Integer.parseInt(gkId));
}
// http://geokretymap.org/38545
final String gkmapId = StringUtils.substringAfterLast(url, "geokretymap.org/");
if (StringUtils.isNumeric(gkmapId)) {
return geocode(Integer.parseInt(gkmapId));
}
return null;
}
@Override
@Nullable
public String getTrackableTrackingCodeFromUrl(@NonNull final String url) {
// http://geokrety.org/m/qr.php?nr=<TRACKING_CODE>
final String gkTrackingCode = StringUtils.substringAfterLast(url, "qr.php?nr=");
if (StringUtils.isAlphanumeric(gkTrackingCode)) {
return gkTrackingCode;
}
return null;
}
/**
* Lookup Trackable Geocode from Tracking Code.
*
* @param trackingCode
* the Trackable Tracking Code to lookup
* @return
* the Trackable Geocode
*/
@Nullable
private static String getGeocodeFromTrackingCode(final String trackingCode) {
final Parameters params = new Parameters("nr", trackingCode);
final String response = Network.getResponseData(Network.getRequest(URLPROXY + "/nr2id.php", params));
// An empty response means "not found"
if (response == null || StringUtils.equals(response, "0")) {
return null;
}
return geocode(Integer.parseInt(response));
}
@Override
@NonNull
public TrackableBrand getBrand() {
return TrackableBrand.GEOKRETY;
}
@Override
public boolean isGenericLoggable() {
return true;
}
@Override
public boolean isActive() {
return Settings.isGeokretyConnectorActive();
}
@Override
public boolean isRegistered() {
return Settings.isRegisteredForGeokretyLogging() && isActive();
}
@Override
public boolean recommendLogWithGeocode() {
return true;
}
@Override
public AbstractTrackableLoggingManager getTrackableLoggingManager(final AbstractLoggingActivity activity) {
return new GeokretyLoggingManager(activity);
}
/**
* Get geocode from GeoKrety id
*
*/
public static String geocode(final int id) {
return String.format("GK%04X", id);
}
@Override
public boolean isLoggable() {
return true;
}
public static ImmutablePair<StatusCode, List<String>> postLogTrackable(final Context context, final Geocache cache, final TrackableLog trackableLog, final Calendar date, final String log) {
// See doc: http://geokrety.org/api.php
Log.d("GeokretyConnector.postLogTrackable: nr=" + trackableLog.trackCode);
if (trackableLog.brand != TrackableBrand.GEOKRETY) {
Log.d("GeokretyConnector.postLogTrackable: received invalid brand");
return new ImmutablePair<>(StatusCode.LOG_POST_ERROR_GK, Collections.<String> emptyList());
}
if (trackableLog.action == LogTypeTrackable.DO_NOTHING) {
Log.d("GeokretyConnector.postLogTrackable: received invalid logtype");
return new ImmutablePair<>(StatusCode.LOG_POST_ERROR_GK, Collections.<String> emptyList());
}
try {
// SecId is mandatory when using API, anonymous log are only possible via website
final String secId = Settings.getGeokretySecId();
if (StringUtils.isEmpty(secId)) {
Log.d("GeokretyConnector.postLogTrackable: not authenticated");
return new ImmutablePair<>(StatusCode.NO_LOGIN_INFO_STORED, Collections.<String> emptyList());
}
// Construct Post Parameters
final Parameters params = new Parameters(
"secid", secId,
"gzip", "0",
"nr", trackableLog.trackCode,
"formname", "ruchy",
"logtype", String.valueOf(trackableLog.action.gkid),
"data", String.format(Locale.ENGLISH, "%tY-%tm-%td", date, date, date), // YYYY-MM-DD
"godzina", String.format("%tH", date), // HH
"minuta", String.format("%tM", date), // MM
"comment", log,
"app", context.getString(R.string.app_name),
"app_ver", Version.getVersionName(context),
"mobile_lang", Settings.getApplicationLocale().toString() + ".UTF-8"
);
// See doc: http://geokrety.org/help.php#acceptableformats
if (cache != null) {
final Geopoint coords = cache.getCoords();
if (coords != null) {
params.add("latlon", coords.toString());
}
final String geocode = cache.getGeocode();
if (StringUtils.isNotEmpty(geocode)) {
params.add("wpt", geocode);
}
}
final String page = Network.getResponseData(Network.postRequest(URL + "/ruchy.php", params));
if (page == null) {
Log.d("GeokretyConnector.postLogTrackable: No data from server");
return new ImmutablePair<>(StatusCode.CONNECTION_FAILED_GK, Collections.<String> emptyList());
}
final ImmutablePair<Integer, List<String>> response = GeokretyParser.parseResponse(page);
if (response == null) {
Log.w("GeokretyConnector.postLogTrackable: Cannot parseResponse GeoKrety");
return new ImmutablePair<>(StatusCode.LOG_POST_ERROR_GK, Collections.<String> emptyList());
}
final List<String> errors = response.getRight();
if (CollectionUtils.isNotEmpty(errors)) {
for (final String error: errors) {
Log.w("GeokretyConnector.postLogTrackable: " + error);
}
return new ImmutablePair<>(StatusCode.LOG_POST_ERROR_GK, errors);
}
Log.i("Geokrety Log successfully posted to trackable #" + trackableLog.trackCode);
return new ImmutablePair<>(StatusCode.NO_ERROR, Collections.<String> emptyList());
} catch (final RuntimeException e) {
Log.w("GeokretyConnector.searchTrackable", e);
return new ImmutablePair<>(StatusCode.LOG_POST_ERROR_GK, Collections.<String> emptyList());
}
}
public static String getCreateAccountUrl() {
return URL + "/adduser.php";
}
}