package cgeo.geocaching.connector.oc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.LogResult; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.oc.OCApiConnector.ApiSupport; import cgeo.geocaching.connector.oc.OCApiConnector.OAuthLevel; import cgeo.geocaching.connector.oc.UserInfo.UserInfoStatus; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.location.GeopointFormatter; import cgeo.geocaching.location.Viewport; import cgeo.geocaching.log.LogEntry; import cgeo.geocaching.log.LogType; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Image; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.models.Waypoint; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.OAuth; import cgeo.geocaching.network.OAuthTokens; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.SynchronizedDateFormat; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.regex.Pattern; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import okhttp3.Response; import org.apache.commons.lang3.StringUtils; /** * Client for the OpenCaching API (Okapi). * * @see <a href="http://www.opencaching.de/okapi/introduction.html">Okapi overview</a> * */ final class OkapiClient { private static final String PARAMETER_LOGCOUNT_KEY = "lpc"; private static final String PARAMETER_LOGCOUNT_VALUE = "all"; private static final String PARAMETER_LOG_FIELDS_KEY = "log_fields"; private static final String PARAMETER_LOG_FIELDS_VALUE = "uuid|date|user|type|comment|images"; private static final char SEPARATOR = '|'; private static final String SEPARATOR_STRING = Character.toString(SEPARATOR); private static final SynchronizedDateFormat LOG_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); private static final SynchronizedDateFormat ISO8601DATEFORMAT = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); private static final String CACHE_ATTRNAMES = "attrnames"; private static final String CACHE_ATTR_ACODES = "attr_acodes"; private static final String WPT_LOCATION = "location"; private static final String WPT_DESCRIPTION = "description"; private static final String WPT_TYPE = "type"; private static final String WPT_NAME = "name"; private static final String CACHE_IS_WATCHED = "is_watched"; private static final String CACHE_WPTS = "alt_wpts"; private static final String CACHE_STATUS_ARCHIVED = "Archived"; private static final String CACHE_STATUS_DISABLED = "Temporarily unavailable"; private static final String CACHE_IS_FOUND = "is_found"; private static final String CACHE_SIZE_DEPRECATED = "size"; private static final String CACHE_SIZE2 = "size2"; private static final String CACHE_VOTES = "rating_votes"; private static final String CACHE_NOTFOUNDS = "notfounds"; private static final String CACHE_FOUNDS = "founds"; private static final String CACHE_WILLATTENDS = "willattends"; private static final String CACHE_HIDDEN = "date_hidden"; private static final String CACHE_LATEST_LOGS = "latest_logs"; private static final String CACHE_IMAGE_URL = "url"; private static final String CACHE_IMAGE_CAPTION = "caption"; private static final String CACHE_IMAGES = "images"; private static final String CACHE_HINT = "hint"; private static final String CACHE_DESCRIPTION = "description"; private static final String CACHE_SHORT_DESCRIPTION = "short_description"; private static final String CACHE_RECOMMENDATIONS = "recommendations"; private static final String CACHE_RATING = "rating"; private static final String CACHE_TERRAIN = "terrain"; private static final String CACHE_DIFFICULTY = "difficulty"; private static final String CACHE_OWNER = "owner"; private static final String CACHE_STATUS = "status"; private static final String CACHE_TYPE = "type"; private static final String CACHE_LOCATION = "location"; private static final String CACHE_NAME = "name"; private static final String CACHE_CODE = "code"; private static final String CACHE_REQ_PASSWORD = "req_passwd"; private static final String CACHE_MY_NOTES = "my_notes"; private static final String CACHE_TRACKABLES_COUNT = "trackables_count"; private static final String CACHE_TRACKABLES = "trackables"; private static final String TRK_GEOCODE = "code"; private static final String TRK_NAME = "name"; private static final String LOG_TYPE = "type"; private static final String LOG_COMMENT = "comment"; private static final String LOG_DATE = "date"; private static final String LOG_USER = "user"; private static final String LOG_IMAGES = "images"; private static final String USER_UUID = "uuid"; private static final String USER_USERNAME = "username"; private static final String USER_CACHES_FOUND = "caches_found"; private static final String USER_INFO_FIELDS = "username|caches_found"; private static final String IMAGE_CAPTION = "caption"; private static final String IMAGE_URL = "url"; // the several realms of possible fields for cache retrieval: // Core: for livemap requests (L3 - only with level 3 auth) // Additional: additional fields for full cache (L3 - only for level 3 auth, current - only for connectors with current api) private static final String SERVICE_CACHE_CORE_FIELDS = "code|name|location|type|status|difficulty|terrain|size|size2|date_hidden|trackables_count"; private static final String SERVICE_CACHE_CORE_L3_FIELDS = "is_found"; private static final String SERVICE_CACHE_ADDITIONAL_FIELDS = "owner|founds|notfounds|rating|rating_votes|recommendations|description|hint|images|latest_logs|alt_wpts|attrnames|req_passwd|trackables"; private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS = "gc_code|attribution_note|attr_acodes|willattends|short_description"; private static final String SERVICE_CACHE_ADDITIONAL_L3_FIELDS = "my_notes"; private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_L3_FIELDS = "is_watched"; private static final String METHOD_SEARCH_ALL = "services/caches/search/all"; private static final String METHOD_SEARCH_BBOX = "services/caches/search/bbox"; private static final String METHOD_SEARCH_NEAREST = "services/caches/search/nearest"; private static final String METHOD_RETRIEVE_CACHES = "services/caches/geocaches"; private static final Pattern PATTERN_TIMEZONE = Pattern.compile("([+-][01][0-9]):([03])0"); private OkapiClient() { // utility class } @Nullable public static Geocache getCache(final String geoCode) { final IConnector connector = ConnectorFactory.getConnector(geoCode); if (!(connector instanceof OCApiConnector)) { return null; } final OCApiConnector ocapiConn = (OCApiConnector) connector; final Parameters params = new Parameters("cache_code", geoCode); params.add("fields", getFullFields(ocapiConn)); params.add("attribution_append", "none"); params.add(PARAMETER_LOGCOUNT_KEY, PARAMETER_LOGCOUNT_VALUE); params.add(PARAMETER_LOG_FIELDS_KEY, PARAMETER_LOG_FIELDS_VALUE); final JSONResult result = request(ocapiConn, OkapiService.SERVICE_CACHE, params); return result.isSuccess ? parseCache(result.data) : null; } @NonNull public static List<Geocache> getCachesAround(@NonNull final Geopoint center, @NonNull final OCApiConnector connector) { final String centerString = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, center) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LON_DECDEGREE_RAW, center); final Parameters params = new Parameters("search_method", METHOD_SEARCH_NEAREST); final Map<String, String> valueMap = new LinkedHashMap<>(); valueMap.put("center", centerString); valueMap.put("limit", getCacheLimit()); valueMap.put("radius", "200"); return requestCaches(connector, params, valueMap, false); } @NonNull public static List<Geocache> getCachesByOwner(@NonNull final String username, @NonNull final OCApiConnector connector) { return getCachesByUser(username, connector, "owner_uuid"); } @NonNull public static List<Geocache> getCachesByFinder(@NonNull final String username, @NonNull final OCApiConnector connector) { return getCachesByUser(username, connector, "found_by"); } @NonNull private static List<Geocache> getCachesByUser(@NonNull final String username, @NonNull final OCApiConnector connector, final String userRequestParam) { final String uuid = getUserUUID(connector, username); if (StringUtils.isEmpty(uuid)) { return Collections.emptyList(); } final Parameters params = new Parameters("search_method", METHOD_SEARCH_ALL); final Map<String, String> valueMap = new LinkedHashMap<>(); valueMap.put(userRequestParam, uuid); return requestCaches(connector, params, valueMap, connector.isSearchForMyCaches(username)); } @NonNull public static List<Geocache> getCachesNamed(@Nullable final Geopoint center, final String namePart, @NonNull final OCApiConnector connector) { final Map<String, String> valueMap = new LinkedHashMap<>(); final Parameters params; // search around current position, if there is a position if (center != null) { final String centerString = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, center) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LON_DECDEGREE_RAW, center); params = new Parameters("search_method", METHOD_SEARCH_NEAREST); valueMap.put("center", centerString); } else { params = new Parameters("search_method", METHOD_SEARCH_ALL); } valueMap.put("limit", getCacheLimit()); // full wildcard search, maybe we need to change this after some testing and evaluation valueMap.put("name", "*" + namePart + "*"); return requestCaches(connector, params, valueMap, false); } @NonNull private static List<Geocache> requestCaches(@NonNull final OCApiConnector connector, @NonNull final Parameters params, @NonNull final Map<String, String> valueMap, final boolean my) { // if a global type filter is set, and OKAPI does not know that type, then return an empty list instead of all caches if (Settings.getCacheType() != CacheType.ALL && StringUtils.isBlank(getFilterFromType())) { return Collections.emptyList(); } addFilterParams(valueMap, connector, my); try { params.add("search_params", JsonUtils.writer.writeValueAsString(valueMap)); } catch (final JsonProcessingException e) { Log.e("requestCaches", e); return Collections.emptyList(); } addRetrieveParams(params, connector); final ObjectNode data = request(connector, OkapiService.SERVICE_SEARCH_AND_RETRIEVE, params).data; if (data == null) { return Collections.emptyList(); } return parseCaches(data); } /** * Assumes level 3 OAuth. */ @NonNull public static List<Geocache> getCachesBBox(final Viewport viewport, @NonNull final OCApiConnector connector) { if (viewport.getLatitudeSpan() == 0 || viewport.getLongitudeSpan() == 0) { return Collections.emptyList(); } final String bboxString = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, viewport.bottomLeft) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LON_DECDEGREE_RAW, viewport.bottomLeft) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, viewport.topRight) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LON_DECDEGREE_RAW, viewport.topRight); final Parameters params = new Parameters("search_method", METHOD_SEARCH_BBOX); final Map<String, String> valueMap = new LinkedHashMap<>(); valueMap.put("bbox", bboxString); return requestCaches(connector, params, valueMap, false); } public static boolean setWatchState(@NonNull final Geocache cache, final boolean watched, @NonNull final OCApiConnector connector) { final Parameters params = new Parameters("cache_code", cache.getGeocode()); params.add("watched", watched ? "true" : "false"); final ObjectNode data = request(connector, OkapiService.SERVICE_MARK_CACHE, params).data; if (data == null) { return false; } cache.setOnWatchlist(watched); return true; } public static boolean setIgnored(@NonNull final Geocache cache, @NonNull final OCApiConnector connector) { final Parameters params = new Parameters("cache_code", cache.getGeocode()); params.add("ignored", "true"); final ObjectNode data = request(connector, OkapiService.SERVICE_MARK_CACHE, params).data; return data != null; } @NonNull public static LogResult postLog(@NonNull final Geocache cache, @NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, @Nullable final String logPassword, @NonNull final OCApiConnector connector) { final Parameters params = new Parameters("cache_code", cache.getGeocode()); params.add("logtype", logType.ocType); params.add("comment", log); params.add("comment_format", "plaintext"); params.add("when", LOG_DATE_FORMAT.format(date.getTime())); if (logType == LogType.NEEDS_MAINTENANCE) { params.add("needs_maintenance", "true"); } if (logPassword != null) { params.add("password", logPassword); } final ObjectNode data = request(connector, OkapiService.SERVICE_SUBMIT_LOG, params).data; if (data == null) { return new LogResult(StatusCode.LOG_POST_ERROR, ""); } try { if (data.get("success").asBoolean()) { return new LogResult(StatusCode.NO_ERROR, data.get("log_uuid").asText()); } return new LogResult(StatusCode.LOG_POST_ERROR, ""); } catch (final NullPointerException e) { Log.e("OkapiClient.postLog", e); } return new LogResult(StatusCode.LOG_POST_ERROR, ""); } public static boolean uploadPersonalNotes(@NonNull final OCApiConnector connector, @NonNull final Geocache cache) { Log.d("Uploading personal note for opencaching"); final Parameters notesParam = new Parameters("cache_code", cache.getGeocode(), "fields", CACHE_MY_NOTES); final ObjectNode notesData = request(connector, OkapiService.SERVICE_CACHE, notesParam).data; String prevNote = StringUtils.EMPTY; if (notesData != null && notesData.get(CACHE_MY_NOTES) != null) { prevNote = notesData.get(CACHE_MY_NOTES).asText(); } final String currentNote = StringUtils.defaultString(cache.getPersonalNote()); final Parameters params = new Parameters("cache_code", cache.getGeocode(), "new_value", currentNote, "old_value", prevNote); final ObjectNode data = request(connector, OkapiService.SERVICE_UPLOAD_PERSONAL_NOTE, params).data; if (data == null) { return false; } if (data.get("replaced").asBoolean()) { Log.d("Successfully uploaded"); return true; } return false; } @NonNull private static List<Geocache> parseCaches(final ObjectNode response) { try { // Check for empty result final JsonNode results = response.path("results"); if (!results.isObject()) { return Collections.emptyList(); } // Get and iterate result list final List<Geocache> caches = new ArrayList<>(results.size()); for (final JsonNode cache: results) { caches.add(parseSmallCache((ObjectNode) cache)); } return caches; } catch (ClassCastException | NullPointerException e) { Log.e("OkapiClient.parseCachesResult", e); } return Collections.emptyList(); } @NonNull private static Geocache parseSmallCache(final ObjectNode response) { final Geocache cache = new Geocache(); cache.setReliableLatLon(true); try { parseCoreCache(response, cache); DataStore.saveCache(cache, EnumSet.of(SaveFlag.CACHE)); } catch (final NullPointerException e) { // FIXME: here we may return a partially filled cache Log.e("OkapiClient.parseSmallCache", e); } return cache; } @NonNull private static Geocache parseCache(final ObjectNode response) { final Geocache cache = new Geocache(); cache.setReliableLatLon(true); try { parseCoreCache(response, cache); // not used: url final String owner = parseUser(response.get(CACHE_OWNER)); cache.setOwnerDisplayName(owner); // OpenCaching has no distinction between user id and user display name. Set the ID anyway to simplify c:geo workflows. cache.setOwnerUserId(owner); final Map<LogType, Integer> logCounts = cache.getLogCounts(); logCounts.put(LogType.FOUND_IT, response.get(CACHE_FOUNDS).asInt()); logCounts.put(LogType.DIDNT_FIND_IT, response.get(CACHE_NOTFOUNDS).asInt()); // only current Api logCounts.put(LogType.WILL_ATTEND, response.path(CACHE_WILLATTENDS).asInt()); if (response.has(CACHE_RATING)) { cache.setRating((float) response.get(CACHE_RATING).asDouble()); } cache.setVotes(response.get(CACHE_VOTES).asInt()); cache.setFavoritePoints(response.get(CACHE_RECOMMENDATIONS).asInt()); // not used: req_password // Prepend gc-link to description if available final StringBuilder description = new StringBuilder(500); if (response.hasNonNull("gc_code")) { final String gccode = response.get("gc_code").asText(); description.append(CgeoApplication.getInstance().getResources() .getString(R.string.cache_listed_on, GCConnector.getInstance().getName())) .append(": <a href=\"https://coord.info/") .append(gccode) .append("\">") .append(gccode) .append("</a><br /><br />"); } description.append(response.get(CACHE_DESCRIPTION).asText()); cache.setDescription(description.toString()); if (response.has(CACHE_SHORT_DESCRIPTION)) { final String shortDescription = StringUtils.trim(response.get(CACHE_SHORT_DESCRIPTION).asText()); if (StringUtils.isNotEmpty(shortDescription)) { cache.setShortDescription(shortDescription); } } // currently the hint is delivered as HTML (contrary to OKAPI documentation), so we can store it directly cache.setHint(response.get(CACHE_HINT).asText()); // not used: hints final ArrayNode images = (ArrayNode) response.get(CACHE_IMAGES); if (images != null) { for (final JsonNode imageResponse: images) { final String title = imageResponse.get(CACHE_IMAGE_CAPTION).asText(); final String url = absoluteUrl(imageResponse.get(CACHE_IMAGE_URL).asText(), cache.getGeocode()); // all images are added as spoiler images, although OKAPI has spoiler and non spoiler images cache.addSpoiler(new Image.Builder().setUrl(url).setTitle(title).build()); } } cache.setAttributes(parseAttributes((ArrayNode) response.path(CACHE_ATTRNAMES), (ArrayNode) response.get(CACHE_ATTR_ACODES))); //TODO: Store license per cache //cache.setLicense(response.getString("attribution_note")); cache.setWaypoints(parseWaypoints((ArrayNode) response.path(CACHE_WPTS)), false); cache.mergeInventory(parseTrackables((ArrayNode) response.path(CACHE_TRACKABLES)), EnumSet.of(TrackableBrand.GEOKRETY)); if (response.has(CACHE_IS_WATCHED)) { cache.setOnWatchlist(response.get(CACHE_IS_WATCHED).asBoolean()); } if (response.hasNonNull(CACHE_MY_NOTES)) { cache.setPersonalNote(response.get(CACHE_MY_NOTES).asText()); } cache.setLogPasswordRequired(response.get(CACHE_REQ_PASSWORD).asBoolean()); cache.setDetailedUpdatedNow(); // save full detailed caches DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); DataStore.saveLogs(cache.getGeocode(), parseLogs((ArrayNode) response.path(CACHE_LATEST_LOGS), cache.getGeocode())); } catch (ClassCastException | NullPointerException e) { Log.e("OkapiClient.parseCache", e); } return cache; } private static void parseCoreCache(final ObjectNode response, @NonNull final Geocache cache) { cache.setGeocode(response.get(CACHE_CODE).asText()); cache.setName(response.get(CACHE_NAME).asText()); // not used: names setLocation(cache, response.get(CACHE_LOCATION).asText()); cache.setType(getCacheType(response.get(CACHE_TYPE).asText())); final String status = response.get(CACHE_STATUS).asText(); cache.setDisabled(status.equalsIgnoreCase(CACHE_STATUS_DISABLED)); cache.setArchived(status.equalsIgnoreCase(CACHE_STATUS_ARCHIVED)); cache.setSize(getCacheSize(response)); cache.setDifficulty((float) response.get(CACHE_DIFFICULTY).asDouble()); cache.setTerrain((float) response.get(CACHE_TERRAIN).asDouble()); cache.setInventoryItems(response.get(CACHE_TRACKABLES_COUNT).asInt()); if (response.has(CACHE_IS_FOUND)) { cache.setFound(response.get(CACHE_IS_FOUND).asBoolean()); } cache.setHidden(parseDate(response.get(CACHE_HIDDEN).asText())); } private static String absoluteUrl(final String url, final String geocode) { final Uri uri = Uri.parse(url); if (!uri.isAbsolute()) { final IConnector connector = ConnectorFactory.getConnector(geocode); final String hostUrl = connector.getHostUrl(); if (StringUtils.isNotBlank(hostUrl)) { return hostUrl + "/" + url; } } return url; } private static String parseUser(final JsonNode user) { return user.get(USER_USERNAME).asText(); } @NonNull private static List<LogEntry> parseLogs(final ArrayNode logsJSON, final String geocode) { final List<LogEntry> result = new LinkedList<>(); for (final JsonNode logResponse: logsJSON) { try { final Date date = parseDate(logResponse.get(LOG_DATE).asText()); if (date == null) { continue; } final LogEntry log = new LogEntry.Builder() .setAuthor(parseUser(logResponse.get(LOG_USER))) .setDate(date.getTime()) .setLogType(parseLogType(logResponse.get(LOG_TYPE).asText())) .setLogImages(parseLogImages((ArrayNode) logResponse.path(LOG_IMAGES), geocode)) .setLog(logResponse.get(LOG_COMMENT).asText().trim()).build(); result.add(log); } catch (final NullPointerException e) { Log.e("OkapiClient.parseLogs", e); } } return result; } private static List<Image> parseLogImages(final ArrayNode imagesNode, final String geocode) { final List<Image> images = new ArrayList<>(); for (final JsonNode image : imagesNode) { images.add(new Image.Builder().setUrl(absoluteUrl(image.get(IMAGE_URL).asText(), geocode)).setTitle(image.get(IMAGE_CAPTION).asText()).build()); } return images; } @Nullable private static List<Waypoint> parseWaypoints(final ArrayNode wptsJson) { List<Waypoint> result = null; for (final JsonNode wptResponse: wptsJson) { try { final Waypoint wpt = new Waypoint(wptResponse.get(WPT_NAME).asText(), parseWptType(wptResponse.get(WPT_TYPE).asText()), false); wpt.setNote(wptResponse.get(WPT_DESCRIPTION).asText()); final Geopoint pt = parseCoords(wptResponse.get(WPT_LOCATION).asText()); if (pt != null) { wpt.setCoords(pt); } else { wpt.setOriginalCoordsEmpty(true); } if (result == null) { result = new ArrayList<>(); } wpt.setPrefix(wpt.getName()); result.add(wpt); } catch (final NullPointerException e) { Log.e("OkapiClient.parseWaypoints", e); } } return result; } @NonNull private static List<Trackable> parseTrackables(final ArrayNode trackablesJson) { if (trackablesJson.size() == 0) { return Collections.emptyList(); } final List<Trackable> result = new ArrayList<>(); for (final JsonNode trackableResponse: trackablesJson) { try { final Trackable trk = new Trackable(); trk.setGeocode(trackableResponse.get(TRK_GEOCODE).asText()); trk.setName(trackableResponse.get(TRK_NAME).asText()); result.add(trk); } catch (final NullPointerException e) { Log.e("OkapiClient.parseWaypoints", e); } } return result; } @NonNull private static LogType parseLogType(@Nullable final String logType) { if ("Found it".equalsIgnoreCase(logType)) { return LogType.FOUND_IT; } if ("Didn't find it".equalsIgnoreCase(logType)) { return LogType.DIDNT_FIND_IT; } if ("Will attend".equalsIgnoreCase(logType)) { return LogType.WILL_ATTEND; } if ("Attended".equalsIgnoreCase(logType)) { return LogType.ATTENDED; } if ("Temporarily unavailable".equalsIgnoreCase(logType)) { return LogType.TEMP_DISABLE_LISTING; } if ("Ready to search".equalsIgnoreCase(logType)) { return LogType.ENABLE_LISTING; } if ("Archived".equalsIgnoreCase(logType)) { return LogType.ARCHIVE; } if ("Locked".equalsIgnoreCase(logType)) { return LogType.ARCHIVE; } if ("Needs maintenance".equalsIgnoreCase(logType)) { return LogType.NEEDS_MAINTENANCE; } if ("Maintenance performed".equalsIgnoreCase(logType)) { return LogType.OWNER_MAINTENANCE; } if ("Moved".equalsIgnoreCase(logType)) { return LogType.UPDATE_COORDINATES; } if ("OC Team comment".equalsIgnoreCase(logType)) { return LogType.POST_REVIEWER_NOTE; } return LogType.NOTE; } @NonNull private static WaypointType parseWptType(@Nullable final String wptType) { if ("parking".equalsIgnoreCase(wptType)) { return WaypointType.PARKING; } if ("path".equalsIgnoreCase(wptType)) { return WaypointType.TRAILHEAD; } if ("stage".equalsIgnoreCase(wptType)) { return WaypointType.STAGE; } if ("physical-stage".equalsIgnoreCase(wptType)) { return WaypointType.STAGE; } if ("virtual-stage".equalsIgnoreCase(wptType)) { return WaypointType.PUZZLE; } if ("final".equalsIgnoreCase(wptType)) { return WaypointType.FINAL; } if ("poi".equalsIgnoreCase(wptType)) { return WaypointType.TRAILHEAD; } return WaypointType.WAYPOINT; } @Nullable private static Date parseDate(final String date) { final String strippedDate = PATTERN_TIMEZONE.matcher(date).replaceAll("$1$20"); try { return ISO8601DATEFORMAT.parse(strippedDate); } catch (final ParseException e) { Log.e("OkapiClient.parseDate", e); } return null; } @Nullable private static Geopoint parseCoords(final String location) { final String latitude = StringUtils.substringBefore(location, SEPARATOR_STRING); final String longitude = StringUtils.substringAfter(location, SEPARATOR_STRING); if (StringUtils.isNotBlank(latitude) && StringUtils.isNotBlank(longitude)) { return new Geopoint(latitude, longitude); } return null; } @NonNull private static List<String> parseAttributes(final ArrayNode nameList, final ArrayNode acodeList) { final List<String> result = new ArrayList<>(); for (int i = 0; i < nameList.size(); i++) { try { final String name = nameList.get(i).asText(); final int acode = acodeList != null ? Integer.parseInt(acodeList.get(i).asText().substring(1)) : CacheAttribute.NO_ID; final CacheAttribute attr = CacheAttribute.getByOcACode(acode); if (attr != null) { result.add(attr.rawName); } else { result.add(name); } } catch (final NullPointerException e) { Log.e("OkapiClient.parseAttributes", e); } } return result; } private static void setLocation(@NonNull final Geocache cache, final String location) { final String latitude = StringUtils.substringBefore(location, SEPARATOR_STRING); final String longitude = StringUtils.substringAfter(location, SEPARATOR_STRING); cache.setCoords(new Geopoint(latitude, longitude)); } @NonNull private static CacheSize getCacheSize(final ObjectNode response) { if (!response.has(CACHE_SIZE2)) { return getCacheSizeDeprecated(response); } try { final String size = response.get(CACHE_SIZE2).asText(); return CacheSize.getById(size); } catch (final NullPointerException e) { Log.e("OkapiClient.getCacheSize", e); return getCacheSizeDeprecated(response); } } @NonNull private static CacheSize getCacheSizeDeprecated(final ObjectNode response) { if (!response.has(CACHE_SIZE_DEPRECATED)) { return CacheSize.NOT_CHOSEN; } double size = 0; try { size = response.get(CACHE_SIZE_DEPRECATED).asDouble(); } catch (final NullPointerException e) { Log.e("OkapiClient.getCacheSize", e); } switch ((int) Math.round(size)) { case 1: return CacheSize.MICRO; case 2: return CacheSize.SMALL; case 3: return CacheSize.REGULAR; case 4: return CacheSize.LARGE; case 5: return CacheSize.VERY_LARGE; default: break; } return CacheSize.NOT_CHOSEN; } @NonNull private static CacheType getCacheType(@Nullable final String cacheType) { if ("Traditional".equalsIgnoreCase(cacheType)) { return CacheType.TRADITIONAL; } if ("Multi".equalsIgnoreCase(cacheType)) { return CacheType.MULTI; } if ("Quiz".equalsIgnoreCase(cacheType)) { return CacheType.MYSTERY; } if ("Virtual".equalsIgnoreCase(cacheType)) { return CacheType.VIRTUAL; } if ("Event".equalsIgnoreCase(cacheType)) { return CacheType.EVENT; } if ("Webcam".equalsIgnoreCase(cacheType)) { return CacheType.WEBCAM; } if ("Math/Physics".equalsIgnoreCase(cacheType)) { return CacheType.MYSTERY; } if ("Drive-In".equalsIgnoreCase(cacheType)) { return CacheType.TRADITIONAL; } return CacheType.UNKNOWN; } @NonNull private static String getCoreFields(@NonNull final OCApiConnector connector) { if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { return SERVICE_CACHE_CORE_FIELDS + SEPARATOR + SERVICE_CACHE_CORE_L3_FIELDS; } return SERVICE_CACHE_CORE_FIELDS; } @NonNull private static String getFullFields(@NonNull final OCApiConnector connector) { final StringBuilder res = new StringBuilder(500); res.append(SERVICE_CACHE_CORE_FIELDS); res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_FIELDS); if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { res.append(SEPARATOR).append(SERVICE_CACHE_CORE_L3_FIELDS); res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_L3_FIELDS); } if (connector.getApiSupport() == ApiSupport.current) { res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS); if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_CURRENT_L3_FIELDS); } } return res.toString(); } @NonNull private static JSONResult request(@NonNull final OCApiConnector connector, @NonNull final OkapiService service, @NonNull final Parameters params) { final String host = connector.getHost(); if (StringUtils.isBlank(host)) { return new JSONResult("unknown OKAPI connector host"); } params.add("langpref", getPreferredLanguage()); switch (connector.getSupportedAuthLevel()) { case Level3: { final OAuthTokens tokens = new OAuthTokens(connector); if (!tokens.isValid()) { return new JSONResult("invalid oauth tokens"); } OAuth.signOAuth(host, service.methodName, "GET", connector.getHttps(), params, tokens, connector.getCK(), connector.getCS()); break; } case Level1 : { connector.addAuthentication(params); break; } default: // do nothing, anonymous access break; } final String uri = connector.getHostUrl() + service.methodName; try { return new JSONResult(Network.getRequest(uri, params).blockingGet()); } catch (final Exception e) { return new JSONResult("connection error"); } } /** * Return a pipe-separated list of preferred languages. English and the device default language (if different) will * always be in the list. Forcing cgeo language to English will prefer English over the device default language. */ @NonNull static String getPreferredLanguage() { final String defaultLanguage = StringUtils.defaultIfBlank(StringUtils.lowerCase(Locale.getDefault().getLanguage()), "en"); if ("en".equals(defaultLanguage)) { return defaultLanguage; } return Settings.useEnglish() ? "en|" + defaultLanguage : defaultLanguage + "|en"; } private static void addFilterParams(@NonNull final Map<String, String> valueMap, @NonNull final OCApiConnector connector, final boolean my) { if (!Settings.isExcludeDisabledCaches()) { valueMap.put("status", "Available|Temporarily unavailable"); } if (!my && Settings.isExcludeMyCaches() && connector.getSupportedAuthLevel() == OAuthLevel.Level3) { valueMap.put("exclude_my_own", "true"); valueMap.put("found_status", "notfound_only"); } // OKAPI returns ignored caches, we have to actively suppress them if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { valueMap.put("ignored_status", "notignored_only"); } if (Settings.getCacheType() != CacheType.ALL) { valueMap.put("type", getFilterFromType()); } } private static void addRetrieveParams(@NonNull final Parameters params, @NonNull final OCApiConnector connector) { params.add("retr_method", METHOD_RETRIEVE_CACHES); params.add("retr_params", "{\"fields\": \"" + getCoreFields(connector) + "\"}"); params.add("wrap", "true"); } @NonNull private static String getFilterFromType() { switch (Settings.getCacheType()) { case EVENT: return "Event"; case MULTI: return "Multi"; case MYSTERY: return "Quiz"; case TRADITIONAL: return "Traditional"; case VIRTUAL: return "Virtual"; case WEBCAM: return "Webcam"; default: return ""; } } @Nullable public static String getUserUUID(@NonNull final OCApiConnector connector, @NonNull final String userName) { final Parameters params = new Parameters("fields", USER_UUID, USER_USERNAME, userName); final JSONResult result = request(connector, OkapiService.SERVICE_USER_BY_USERNAME, params); if (!result.isSuccess) { final OkapiError error = new OkapiError(result.data); Log.e("OkapiClient.getUserUUID: error getting user info: '" + error.getMessage() + "'"); return null; } return result.data.path(USER_UUID).asText(null); } @NonNull public static UserInfo getUserInfo(@NonNull final OCApiLiveConnector connector) { final Parameters params = new Parameters("fields", USER_INFO_FIELDS); final JSONResult result = request(connector, OkapiService.SERVICE_USER, params); if (!result.isSuccess) { final OkapiError error = new OkapiError(result.data); Log.e("OkapiClient.getUserInfo: error getting user info: '" + error.getMessage() + "'"); return new UserInfo(StringUtils.EMPTY, 0, UserInfoStatus.getFromOkapiError(error.getResult())); } final ObjectNode data = result.data; final boolean successUserName = data.has(USER_USERNAME); final String name = data.path(USER_USERNAME).asText(); final boolean successFinds = data.has(USER_CACHES_FOUND); final int finds = data.path(USER_CACHES_FOUND).asInt(); return new UserInfo(name, finds, successUserName && successFinds ? UserInfoStatus.SUCCESSFUL : UserInfoStatus.FAILED); } /** * Retrieves error information from an unsuccessful Okapi-response * * @param response * response containing an error object * @return OkapiError object with detailed information */ @NonNull public static OkapiError decodeErrorResponse(final Response response) { final JSONResult result = new JSONResult(response); if (!result.isSuccess) { return new OkapiError(result.data); } return new OkapiError(new ObjectNode(JsonUtils.factory)); } /** * Encapsulates response state and content of an HTTP-request that expects a JSON result. {@code isSuccess} is * only true, if the response state was success and {@code data} is not null. */ private static class JSONResult { public final boolean isSuccess; public final ObjectNode data; JSONResult(final Response response) { ObjectNode tempData = null; try { tempData = (ObjectNode) JsonUtils.reader.readTree(response.body().byteStream()); } catch (final Exception e) { // ignore } finally { response.close(); } data = tempData; isSuccess = response.isSuccessful() && tempData != null; } JSONResult(@NonNull final String errorMessage) { isSuccess = false; data = new ObjectNode(JsonUtils.factory); data.putObject("error").put("developer_message", errorMessage); } } /** * extract the geocode from an URL, by using a backward mapping on the server */ @Nullable public static String getGeocodeByUrl(@NonNull final OCApiConnector connector, @NonNull final String url) { final Parameters params = new Parameters("urls", url); final ObjectNode data = request(connector, OkapiService.SERVICE_RESOLVE_URL, params).data; if (data == null) { return null; } return data.path("results").path(0).asText(null); } /** * get the registration url for mobile devices */ public static String getMobileRegistrationUrl(@NonNull final OCApiConnector connector) { return getInstallationInformation(connector, "mobile_registration_url"); } /** * get the normal registration url */ public static String getRegistrationUrl(@NonNull final OCApiConnector connector) { return getInstallationInformation(connector, "registration_url"); } private static String getInstallationInformation(final OCApiConnector connector, final String field) { final ObjectNode data = request(connector, OkapiService.SERVICE_API_INSTALLATION, new Parameters()).data; if (data == null) { return null; } if (data.has(field) && !data.get(field).isNull()) { return data.get(field).asText(); } return null; } /** * Fetch more caches, if the GC connector is not active at all. */ private static String getCacheLimit() { return GCConnector.getInstance().isActive() ? "20" : "100"; } }