Java tutorial
package cgeo.geocaching.connector.gc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; import cgeo.geocaching.Image; import cgeo.geocaching.LogEntry; import cgeo.geocaching.PocketQueryList; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.Trackable; import cgeo.geocaching.TrackableLog; import cgeo.geocaching.Waypoint; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.LogTypeTrackable; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.files.LocParser; import cgeo.geocaching.gcvote.GCVote; import cgeo.geocaching.gcvote.GCVoteRating; import cgeo.geocaching.geopoint.DistanceParser; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.loaders.RecaptchaReceiver; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.DirectionImage; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; import cgeo.geocaching.utils.SynchronizedDateFormat; import cgeo.geocaching.utils.TextUtils; import ch.boye.httpclientandroidlib.HttpResponse; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.net.Uri; import android.text.Html; import java.io.File; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; public abstract class GCParser { private final static SynchronizedDateFormat dateTbIn1 = new SynchronizedDateFormat("EEEEE, dd MMMMM yyyy", Locale.ENGLISH); // Saturday, 28 March 2009 private final static SynchronizedDateFormat dateTbIn2 = new SynchronizedDateFormat("EEEEE, MMMMM dd, yyyy", Locale.ENGLISH); // Saturday, March 28, 2009 private static SearchResult parseSearch(final String url, final String pageContent, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pageContent)) { Log.e("GCParser.parseSearch: No page given"); return null; } final List<String> cids = new ArrayList<String>(); String page = pageContent; final SearchResult searchResult = new SearchResult(); searchResult.setUrl(url); searchResult.viewstates = GCLogin.getViewstates(page); // recaptcha if (showCaptcha) { final String recaptchaJsParam = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_RECAPTCHA, false, null); if (recaptchaJsParam != null) { recaptchaReceiver.setKey(recaptchaJsParam.trim()); recaptchaReceiver.fetchChallenge(); } if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) { recaptchaReceiver.notifyNeed(); } } if (!page.contains("SearchResultsTable")) { // there are no results. aborting here avoids a wrong error log in the next parsing step return searchResult; } int startPos = page.indexOf("<div id=\"ctl00_ContentBody_ResultsPanel\""); if (startPos == -1) { Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_dlResults\" not found on page"); return null; } page = page.substring(startPos); // cut on <table startPos = page.indexOf('>'); final int endPos = page.indexOf("ctl00_ContentBody_UnitTxt"); if (startPos == -1 || endPos == -1) { Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_UnitTxt\" not found on page"); return null; } page = page.substring(startPos + 1, endPos - startPos + 1); // cut between <table> and </table> final String[] rows = page.split("<tr class="); final int rows_count = rows.length; int excludedCaches = 0; final ArrayList<Geocache> caches = new ArrayList<Geocache>(); for (int z = 1; z < rows_count; z++) { final Geocache cache = new Geocache(); final String row = rows[z]; // check for cache type presence if (!row.contains("images/wpttypes")) { continue; } try { final MatcherWrapper matcherGuidAndDisabled = new MatcherWrapper( GCConstants.PATTERN_SEARCH_GUIDANDDISABLED, row); while (matcherGuidAndDisabled.find()) { if (matcherGuidAndDisabled.groupCount() > 0) { //cache.setGuid(matcherGuidAndDisabled.group(1)); if (matcherGuidAndDisabled.group(2) != null) { cache.setName(Html.fromHtml(matcherGuidAndDisabled.group(2).trim()).toString()); } if (matcherGuidAndDisabled.group(3) != null) { cache.setLocation(Html.fromHtml(matcherGuidAndDisabled.group(3).trim()).toString()); } final String attr = matcherGuidAndDisabled.group(1); if (attr != null) { cache.setDisabled(attr.contains("Strike")); cache.setArchived(attr.contains("OldWarning")); } } } } catch (final RuntimeException e) { // failed to parse GUID and/or Disabled Log.w("GCParser.parseSearch: Failed to parse GUID and/or Disabled data"); } if (Settings.isExcludeDisabledCaches() && (cache.isDisabled() || cache.isArchived())) { // skip disabled and archived caches excludedCaches++; continue; } cache.setGeocode( TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_GEOCODE, true, 1, cache.getGeocode(), true)); // cache type cache.setType(CacheType .getByPattern(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_TYPE, true, 1, null, true))); // cache direction - image if (Settings.getLoadDirImg()) { final String direction = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 1, null, false); if (direction != null) { cache.setDirectionImg(direction); } } // cache distance - estimated distance for basic members final String distance = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 2, null, false); if (distance != null) { cache.setDistance(DistanceParser.parseDistance(distance, !Settings.isUseImperialUnits())); } // difficulty/terrain final MatcherWrapper matcherDT = new MatcherWrapper(GCConstants.PATTERN_SEARCH_DIFFICULTY_TERRAIN, row); if (matcherDT.find()) { final Float difficulty = parseStars(matcherDT.group(1)); if (difficulty != null) { cache.setDifficulty(difficulty); } final Float terrain = parseStars(matcherDT.group(3)); if (terrain != null) { cache.setTerrain(terrain); } } // size final String container = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_CONTAINER, false, 1, null, false); cache.setSize(CacheSize.getById(container)); // date hidden, makes sorting event caches easier final String dateHidden = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_HIDDEN_DATE, false, 1, null, false); if (StringUtils.isNotBlank(dateHidden)) { try { Date date = GCLogin.parseGcCustomDate(dateHidden); if (date != null) { cache.setHidden(date); } } catch (ParseException e) { Log.e("Error parsing event date from search"); } } // cache inventory final MatcherWrapper matcherTbs = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLES, row); String inventoryPre = null; while (matcherTbs.find()) { if (matcherTbs.groupCount() > 0) { try { cache.setInventoryItems(Integer.parseInt(matcherTbs.group(1))); } catch (final NumberFormatException e) { Log.e("Error parsing trackables count", e); } inventoryPre = matcherTbs.group(2); } } if (StringUtils.isNotBlank(inventoryPre)) { final MatcherWrapper matcherTbsInside = new MatcherWrapper( GCConstants.PATTERN_SEARCH_TRACKABLESINSIDE, inventoryPre); while (matcherTbsInside.find()) { if (matcherTbsInside.groupCount() == 2 && matcherTbsInside.group(2) != null && !matcherTbsInside.group(2).equalsIgnoreCase("premium member only cache") && cache.getInventoryItems() <= 0) { cache.setInventoryItems(1); } } } // premium cache cache.setPremiumMembersOnly(row.contains("/images/icons/16/premium_only.png")); // found it cache.setFound(row.contains("/images/icons/16/found.png") || row.contains("uxUserLogDate\" class=\"Success\"")); // id String result = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_ID, null); if (null != result) { cache.setCacheId(result); cids.add(cache.getCacheId()); } // favorite count try { result = getNumberString( TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_FAVORITE, false, 1, null, true)); if (null != result) { cache.setFavoritePoints(Integer.parseInt(result)); } } catch (final NumberFormatException e) { Log.w("GCParser.parseSearch: Failed to parse favorite count"); } caches.add(cache); } searchResult.addAndPutInCache(caches); // total caches found try { final String result = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_TOTALCOUNT, false, 1, null, true); if (null != result) { searchResult.setTotalCountGC(Integer.parseInt(result) - excludedCaches); } } catch (final NumberFormatException e) { Log.w("GCParser.parseSearch: Failed to parse cache count"); } String recaptchaText = null; if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) { recaptchaReceiver.waitForUser(); recaptchaText = recaptchaReceiver.getText(); } if (!cids.isEmpty() && (Settings.isGCPremiumMember() || showCaptcha) && ((recaptchaReceiver == null || StringUtils.isBlank(recaptchaReceiver.getChallenge())) || StringUtils.isNotBlank(recaptchaText))) { Log.i("Trying to get .loc for " + cids.size() + " caches"); try { // get coordinates for parsed caches final Parameters params = new Parameters("__EVENTTARGET", "", "__EVENTARGUMENT", ""); GCLogin.putViewstates(params, searchResult.viewstates); for (final String cid : cids) { params.put("CID", cid); } if (StringUtils.isNotBlank(recaptchaText) && recaptchaReceiver != null) { params.put("recaptcha_challenge_field", recaptchaReceiver.getChallenge()); params.put("recaptcha_response_field", recaptchaText); } params.put("ctl00$ContentBody$uxDownloadLoc", "Download Waypoints"); final String coordinates = Network.getResponseData( Network.postRequest("http://www.geocaching.com/seek/nearest.aspx", params), false); if (StringUtils.contains(coordinates, "You have not agreed to the license agreement. The license agreement is required before you can start downloading GPX or LOC files from Geocaching.com")) { Log.i("User has not agreed to the license agreement. Can\'t download .loc file."); searchResult.setError(StatusCode.UNAPPROVED_LICENSE); return searchResult; } LocParser.parseLoc(searchResult, coordinates); } catch (final RuntimeException e) { Log.e("GCParser.parseSearch.CIDs", e); } } // get direction images if (Settings.getLoadDirImg()) { final Set<Geocache> cachesReloaded = searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB); for (final Geocache cache : cachesReloaded) { if (cache.getCoords() == null && StringUtils.isNotEmpty(cache.getDirectionImg())) { DirectionImage.getDrawable(cache.getDirectionImg()); } } } return searchResult; } private static Float parseStars(final String value) { final float floatValue = Float.parseFloat(StringUtils.replaceChars(value, ',', '.')); return floatValue >= 0.5 && floatValue <= 5.0 ? floatValue : null; } static SearchResult parseCache(final String page, final CancellableHandler handler) { final SearchResult searchResult = parseCacheFromText(page, handler); // attention: parseCacheFromText already stores implicitly through searchResult.addCache if (searchResult != null && !searchResult.getGeocodes().isEmpty()) { final Geocache cache = searchResult.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB); if (cache == null) { return null; } getExtraOnlineInfo(cache, page, handler); // too late: it is already stored through parseCacheFromText cache.setDetailedUpdatedNow(); if (CancellableHandler.isCancelled(handler)) { return null; } // save full detailed caches CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_cache); DataStore.saveCache(cache, EnumSet.of(SaveFlag.SAVE_DB)); // update progress message so user knows we're still working. This is more of a place holder than // actual indication of what the program is doing CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_render); } return searchResult; } static SearchResult parseCacheFromText(final String pageIn, final CancellableHandler handler) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_details); if (StringUtils.isBlank(pageIn)) { Log.e("GCParser.parseCache: No page given"); return null; } final SearchResult searchResult = new SearchResult(); if (pageIn.contains(GCConstants.STRING_UNPUBLISHED_OTHER) || pageIn.contains(GCConstants.STRING_UNPUBLISHED_FROM_SEARCH)) { searchResult.setError(StatusCode.UNPUBLISHED_CACHE); return searchResult; } if (pageIn.contains(GCConstants.STRING_PREMIUMONLY_1) || pageIn.contains(GCConstants.STRING_PREMIUMONLY_2)) { searchResult.setError(StatusCode.PREMIUM_ONLY); return searchResult; } final String cacheName = Html.fromHtml(TextUtils.getMatch(pageIn, GCConstants.PATTERN_NAME, true, "")) .toString(); if (GCConstants.STRING_UNKNOWN_ERROR.equalsIgnoreCase(cacheName)) { searchResult.setError(StatusCode.UNKNOWN_ERROR); return searchResult; } // first handle the content with line breaks, then trim everything for easier matching and reduced memory consumption in parsed fields String personalNoteWithLineBreaks = ""; MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_PERSONALNOTE, pageIn); if (matcher.find()) { personalNoteWithLineBreaks = matcher.group(1).trim(); } final String page = TextUtils.replaceWhitespace(pageIn); final Geocache cache = new Geocache(); cache.setDisabled(page.contains(GCConstants.STRING_DISABLED)); cache.setArchived(page.contains(GCConstants.STRING_ARCHIVED)); cache.setPremiumMembersOnly(TextUtils.matches(page, GCConstants.PATTERN_PREMIUMMEMBERS)); cache.setFavorite(TextUtils.matches(page, GCConstants.PATTERN_FAVORITE)); // cache geocode cache.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_GEOCODE, true, cache.getGeocode())); // cache id cache.setCacheId(TextUtils.getMatch(page, GCConstants.PATTERN_CACHEID, true, cache.getCacheId())); // cache guid cache.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_GUID, true, cache.getGuid())); // name cache.setName(cacheName); // owner real name cache.setOwnerUserId(Network .decode(TextUtils.getMatch(page, GCConstants.PATTERN_OWNER_USERID, true, cache.getOwnerUserId()))); cache.setUserModifiedCoords(false); String tableInside = page; final int pos = tableInside.indexOf(GCConstants.STRING_CACHEDETAILS); if (pos == -1) { Log.e("GCParser.parseCache: ID \"cacheDetails\" not found on page"); return null; } tableInside = tableInside.substring(pos); if (StringUtils.isNotBlank(tableInside)) { // cache terrain String result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_TERRAIN, true, null); if (result != null) { try { cache.setTerrain(Float.parseFloat(StringUtils.replaceChars(result, '_', '.'))); } catch (final NumberFormatException e) { Log.e("Error parsing terrain value", e); } } // cache difficulty result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_DIFFICULTY, true, null); if (result != null) { try { cache.setDifficulty(Float.parseFloat(StringUtils.replaceChars(result, '_', '.'))); } catch (final NumberFormatException e) { Log.e("Error parsing difficulty value", e); } } // owner cache.setOwnerDisplayName(StringEscapeUtils.unescapeHtml4(TextUtils.getMatch(tableInside, GCConstants.PATTERN_OWNER_DISPLAYNAME, true, cache.getOwnerDisplayName()))); // hidden try { String hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDEN, true, null); if (StringUtils.isNotBlank(hiddenString)) { cache.setHidden(GCLogin.parseGcCustomDate(hiddenString)); } if (cache.getHiddenDate() == null) { // event date hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDENEVENT, true, null); if (StringUtils.isNotBlank(hiddenString)) { cache.setHidden(GCLogin.parseGcCustomDate(hiddenString)); } } } catch (final ParseException e) { // failed to parse cache hidden date Log.w("GCParser.parseCache: Failed to parse cache hidden (event) date"); } // favorite try { cache.setFavoritePoints(Integer .parseInt(TextUtils.getMatch(tableInside, GCConstants.PATTERN_FAVORITECOUNT, true, "0"))); } catch (final NumberFormatException e) { Log.e("Error parsing favorite count", e); } // cache size cache.setSize(CacheSize.getById( TextUtils.getMatch(tableInside, GCConstants.PATTERN_SIZE, true, CacheSize.NOT_CHOSEN.id))); } // cache found cache.setFound(TextUtils.matches(page, GCConstants.PATTERN_FOUND) || TextUtils.matches(page, GCConstants.PATTERN_FOUND_ALTERNATIVE)); // cache found date try { final String foundDateString = TextUtils.getMatch(page, GCConstants.PATTERN_FOUND_DATE, true, null); if (StringUtils.isNotBlank(foundDateString)) { cache.setVisitedDate(GCLogin.parseGcCustomDate(foundDateString).getTime()); } } catch (final ParseException e) { // failed to parse cache found date Log.w("GCParser.parseCache: Failed to parse cache found date"); } // cache type cache.setType(CacheType .getByPattern(TextUtils.getMatch(page, GCConstants.PATTERN_TYPE, true, cache.getType().id))); // on watchlist cache.setOnWatchlist(TextUtils.matches(page, GCConstants.PATTERN_WATCHLIST)); // latitude and longitude. Can only be retrieved if user is logged in String latlon = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON, true, ""); if (StringUtils.isNotEmpty(latlon)) { try { cache.setCoords(new Geopoint(latlon)); cache.setReliableLatLon(true); } catch (final Geopoint.GeopointException e) { Log.w("GCParser.parseCache: Failed to parse cache coordinates", e); } } // cache location cache.setLocation(TextUtils.getMatch(page, GCConstants.PATTERN_LOCATION, true, "")); // cache hint final String result = TextUtils.getMatch(page, GCConstants.PATTERN_HINT, false, null); if (result != null) { // replace linebreak and paragraph tags final String hint = GCConstants.PATTERN_LINEBREAK.matcher(result).replaceAll("\n"); cache.setHint(StringUtils.replace(hint, "</p>", "").trim()); } cache.checkFields(); // cache personal note cache.setPersonalNote(personalNoteWithLineBreaks); // cache short description cache.setShortDescription(TextUtils.getMatch(page, GCConstants.PATTERN_SHORTDESC, true, "")); // cache description cache.setDescription(TextUtils.getMatch(page, GCConstants.PATTERN_DESC, true, "")); // cache attributes try { final String attributesPre = TextUtils.getMatch(page, GCConstants.PATTERN_ATTRIBUTES, true, null); if (null != attributesPre) { final MatcherWrapper matcherAttributesInside = new MatcherWrapper( GCConstants.PATTERN_ATTRIBUTESINSIDE, attributesPre); final ArrayList<String> attributes = new ArrayList<String>(); while (matcherAttributesInside.find()) { if (matcherAttributesInside.groupCount() > 1 && !matcherAttributesInside.group(2).equalsIgnoreCase("blank")) { // by default, use the tooltip of the attribute String attribute = matcherAttributesInside.group(2).toLowerCase(Locale.US); // if the image name can be recognized, use the image name as attribute final String imageName = matcherAttributesInside.group(1).trim(); if (StringUtils.isNotEmpty(imageName)) { final int start = imageName.lastIndexOf('/'); final int end = imageName.lastIndexOf('.'); if (start >= 0 && end >= 0) { attribute = imageName.substring(start + 1, end).replace('-', '_') .toLowerCase(Locale.US); } } attributes.add(attribute); } } cache.setAttributes(attributes); } } catch (final RuntimeException e) { // failed to parse cache attributes Log.w("GCParser.parseCache: Failed to parse cache attributes"); } // cache spoilers try { if (CancellableHandler.isCancelled(handler)) { return null; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_spoilers); final MatcherWrapper matcherSpoilersInside = new MatcherWrapper(GCConstants.PATTERN_SPOILER_IMAGE, page); while (matcherSpoilersInside.find()) { // the original spoiler URL (include .../display/... contains a low-resolution image // if we shorten the URL we get the original-resolution image final String url = matcherSpoilersInside.group(1).replace("/display", ""); String title = null; if (matcherSpoilersInside.group(3) != null) { title = matcherSpoilersInside.group(3); } String description = null; if (matcherSpoilersInside.group(4) != null) { description = matcherSpoilersInside.group(4); } cache.addSpoiler(new Image(url, title, description)); } } catch (final RuntimeException e) { // failed to parse cache spoilers Log.w("GCParser.parseCache: Failed to parse cache spoilers"); } // cache inventory try { cache.setInventoryItems(0); final MatcherWrapper matcherInventory = new MatcherWrapper(GCConstants.PATTERN_INVENTORY, page); if (matcherInventory.find()) { if (cache.getInventory() == null) { cache.setInventory(new ArrayList<Trackable>()); } if (matcherInventory.groupCount() > 1) { final String inventoryPre = matcherInventory.group(2); if (StringUtils.isNotBlank(inventoryPre)) { final MatcherWrapper matcherInventoryInside = new MatcherWrapper( GCConstants.PATTERN_INVENTORYINSIDE, inventoryPre); while (matcherInventoryInside.find()) { if (matcherInventoryInside.groupCount() > 0) { final Trackable inventoryItem = new Trackable(); inventoryItem.setGuid(matcherInventoryInside.group(1)); inventoryItem.setName(matcherInventoryInside.group(2)); cache.getInventory().add(inventoryItem); cache.setInventoryItems(cache.getInventoryItems() + 1); } } } } } } catch (final RuntimeException e) { // failed to parse cache inventory Log.w("GCParser.parseCache: Failed to parse cache inventory (2)"); } // cache logs counts try { final String countlogs = TextUtils.getMatch(page, GCConstants.PATTERN_COUNTLOGS, true, null); if (null != countlogs) { final MatcherWrapper matcherLog = new MatcherWrapper(GCConstants.PATTERN_COUNTLOG, countlogs); while (matcherLog.find()) { final String typeStr = matcherLog.group(1); final String countStr = getNumberString(matcherLog.group(2)); if (StringUtils.isNotBlank(typeStr) && LogType.UNKNOWN != LogType.getByIconName(typeStr) && StringUtils.isNotBlank(countStr)) { cache.getLogCounts().put(LogType.getByIconName(typeStr), Integer.parseInt(countStr)); } } } } catch (final NumberFormatException e) { // failed to parse logs Log.w("GCParser.parseCache: Failed to parse cache log count"); } // waypoints - reset collection cache.setWaypoints(Collections.<Waypoint>emptyList(), false); // add waypoint for original coordinates in case of user-modified listing-coordinates try { final String originalCoords = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON_ORIG, false, null); if (null != originalCoords) { final Waypoint waypoint = new Waypoint( CgeoApplication.getInstance().getString(R.string.cache_coordinates_original), WaypointType.ORIGINAL, false); waypoint.setCoords(new Geopoint(originalCoords)); cache.addOrChangeWaypoint(waypoint, false); cache.setUserModifiedCoords(true); } } catch (final Geopoint.GeopointException e) { } int wpBegin = page.indexOf("<table class=\"Table\" id=\"ctl00_ContentBody_Waypoints\">"); if (wpBegin != -1) { // parse waypoints if (CancellableHandler.isCancelled(handler)) { return null; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_waypoints); String wpList = page.substring(wpBegin); int wpEnd = wpList.indexOf("</p>"); if (wpEnd > -1 && wpEnd <= wpList.length()) { wpList = wpList.substring(0, wpEnd); } if (!wpList.contains("No additional waypoints to display.")) { wpEnd = wpList.indexOf("</table>"); wpList = wpList.substring(0, wpEnd); wpBegin = wpList.indexOf("<tbody>"); wpEnd = wpList.indexOf("</tbody>"); if (wpBegin >= 0 && wpEnd >= 0 && wpEnd <= wpList.length()) { wpList = wpList.substring(wpBegin + 7, wpEnd); } final String[] wpItems = wpList.split("<tr"); for (int j = 1; j < wpItems.length; j++) { String[] wp = wpItems[j].split("<td"); // waypoint name // res is null during the unit tests final String name = TextUtils.getMatch(wp[6], GCConstants.PATTERN_WPNAME, true, 1, CgeoApplication.getInstance().getString(R.string.waypoint), true); // waypoint type final String resulttype = TextUtils.getMatch(wp[3], GCConstants.PATTERN_WPTYPE, null); final Waypoint waypoint = new Waypoint(name, WaypointType.findById(resulttype), false); // waypoint prefix waypoint.setPrefix(TextUtils.getMatch(wp[4], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getPrefix(), false)); // waypoint lookup waypoint.setLookup(TextUtils.getMatch(wp[5], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getLookup(), false)); // waypoint latitude and longitude latlon = Html.fromHtml(TextUtils.getMatch(wp[7], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, false, 2, "", false)).toString().trim(); if (!StringUtils.startsWith(latlon, "???")) { waypoint.setLatlon(latlon); waypoint.setCoords(new Geopoint(latlon)); } j++; if (wpItems.length > j) { wp = wpItems[j].split("<td"); } // waypoint note waypoint.setNote(TextUtils.getMatch(wp[3], GCConstants.PATTERN_WPNOTE, waypoint.getNote())); cache.addOrChangeWaypoint(waypoint, false); } } } cache.parseWaypointsFromNote(); // logs cache.setLogs(getLogsFromDetails(page, false)); // last check for necessary cache conditions if (StringUtils.isBlank(cache.getGeocode())) { searchResult.setError(StatusCode.UNKNOWN_ERROR); return searchResult; } cache.setDetailedUpdatedNow(); searchResult.addAndPutInCache(Collections.singletonList(cache)); return searchResult; } private static String getNumberString(final String numberWithPunctuation) { return StringUtils.replaceChars(numberWithPunctuation, ".,", ""); } public static SearchResult searchByNextPage(final SearchResult search, boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (search == null) { return null; } final String[] viewstates = search.getViewstates(); final String url = search.getUrl(); if (StringUtils.isBlank(url)) { Log.e("GCParser.searchByNextPage: No url found"); return search; } if (GCLogin.isEmpty(viewstates)) { Log.e("GCParser.searchByNextPage: No viewstate given"); return search; } // As in the original code, remove the query string final String uri = Uri.parse(url).buildUpon().query(null).build().toString(); final Parameters params = new Parameters("__EVENTTARGET", "ctl00$ContentBody$pgrBottom$ctl08", "__EVENTARGUMENT", ""); GCLogin.putViewstates(params, viewstates); final String page = GCLogin.getInstance().postRequestLogged(uri, params); if (!GCLogin.getInstance().getLoginStatus(page)) { Log.e("GCParser.postLogTrackable: Can not log in geocaching"); return search; } if (StringUtils.isBlank(page)) { Log.e("GCParser.searchByNextPage: No data from server"); return search; } final SearchResult searchResult = parseSearch(url, page, showCaptcha, recaptchaReceiver); if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) { Log.w("GCParser.searchByNextPage: No cache parsed"); return search; } // search results don't need to be filtered so load GCVote ratings here GCVote.loadRatings( new ArrayList<Geocache>(searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB))); // save to application search.setError(searchResult.getError()); search.setViewstates(searchResult.viewstates); for (final String geocode : searchResult.getGeocodes()) { search.addGeocode(geocode); } return search; } /** * Possibly hide caches found or hidden by user. This mutates its params argument when possible. * * @param params * the parameters to mutate, or null to create a new Parameters if needed * @param my * @param addF * @return the original params if not null, maybe augmented with f=1, or a new Parameters with f=1 or null otherwise */ private static Parameters addFToParams(final Parameters params, final boolean my, final boolean addF) { if (!my && Settings.isExcludeMyCaches() && addF) { if (params == null) { return new Parameters("f", "1"); } params.put("f", "1"); Log.i("Skipping caches found or hidden by user."); } return params; } /** * @param cacheType * @param listId * @param showCaptcha * @param params * the parameters to add to the request URI * @param recaptchaReceiver * @return */ @Nullable private static SearchResult searchByAny(final CacheType cacheType, final boolean my, final boolean showCaptcha, final Parameters params, RecaptchaReceiver recaptchaReceiver) { insertCacheType(params, cacheType); final String uri = "http://www.geocaching.com/seek/nearest.aspx"; final String fullUri = uri + "?" + addFToParams(params, my, true); final String page = GCLogin.getInstance().getRequestLogged(uri, addFToParams(params, my, true)); if (StringUtils.isBlank(page)) { Log.e("GCParser.searchByAny: No data from server"); return null; } assert page != null; final SearchResult searchResult = parseSearch(fullUri, page, showCaptcha, recaptchaReceiver); if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) { Log.e("GCParser.searchByAny: No cache parsed"); return searchResult; } final SearchResult search = searchResult.filterSearchResults(Settings.isExcludeDisabledCaches(), false, cacheType); GCLogin.getInstance().getLoginStatus(page); return search; } public static SearchResult searchByCoords(final @NonNull Geopoint coords, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { final Parameters params = new Parameters("lat", Double.toString(coords.getLatitude()), "lng", Double.toString(coords.getLongitude())); return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver); } public static SearchResult searchByKeyword(final @NonNull String keyword, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(keyword)) { Log.e("GCParser.searchByKeyword: No keyword given"); return null; } final Parameters params = new Parameters("key", keyword); return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver); } private static boolean isSearchForMyCaches(final String userName) { if (userName.equalsIgnoreCase(Settings.getGcCredentials().left)) { Log.i("Overriding users choice because of self search, downloading all caches."); return true; } return false; } public static SearchResult searchByUsername(final String userName, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(userName)) { Log.e("GCParser.searchByUsername: No user name given"); return null; } final Parameters params = new Parameters("ul", userName); return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver); } public static SearchResult searchByPocketQuery(final String pocketGuid, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pocketGuid)) { Log.e("GCParser.searchByPocket: No guid name given"); return null; } final Parameters params = new Parameters("pq", pocketGuid); return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver); } public static SearchResult searchByOwner(final String userName, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(userName)) { Log.e("GCParser.searchByOwner: No user name given"); return null; } final Parameters params = new Parameters("u", userName); return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver); } public static SearchResult searchByAddress(final String address, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(address)) { Log.e("GCParser.searchByAddress: No address given"); return null; } try { final JSONObject response = Network.requestJSON("http://www.geocaching.com/api/geocode", new Parameters("q", address)); if (response == null) { return null; } if (!StringUtils.equalsIgnoreCase(response.getString("status"), "success")) { return null; } if (!response.has("data")) { return null; } final JSONObject data = response.getJSONObject("data"); if (data == null) { return null; } return searchByCoords(new Geopoint(data.getDouble("lat"), data.getDouble("lng")), cacheType, showCaptcha, recaptchaReceiver); } catch (final JSONException e) { Log.w("GCParser.searchByAddress", e); } return null; } @Nullable public static Trackable searchTrackable(final String geocode, final String guid, final String id) { if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid) && StringUtils.isBlank(id)) { Log.w("GCParser.searchTrackable: No geocode nor guid nor id given"); return null; } Trackable trackable = new Trackable(); final Parameters params = new Parameters(); if (StringUtils.isNotBlank(geocode)) { params.put("tracker", geocode); trackable.setGeocode(geocode); } else if (StringUtils.isNotBlank(guid)) { params.put("guid", guid); } else if (StringUtils.isNotBlank(id)) { params.put("id", id); } final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/track/details.aspx", params); if (StringUtils.isBlank(page)) { Log.e("GCParser.searchTrackable: No data from server"); return trackable; } assert page != null; trackable = parseTrackable(page, geocode); if (trackable == null) { Log.w("GCParser.searchTrackable: No trackable parsed"); return null; } return trackable; } public static List<PocketQueryList> searchPocketQueryList() { final Parameters params = new Parameters(); final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/pocket/default.aspx", params); if (StringUtils.isBlank(page)) { Log.e("GCParser.searchPocketQueryList: No data from server"); return null; } String subPage = StringUtils.substringAfter(page, "class=\"PocketQueryListTable"); if (StringUtils.isEmpty(subPage)) { Log.e("GCParser.searchPocketQueryList: class \"PocketQueryListTable\" not found on page"); return Collections.emptyList(); } List<PocketQueryList> list = new ArrayList<PocketQueryList>(); final MatcherWrapper matcherPocket = new MatcherWrapper(GCConstants.PATTERN_LIST_PQ, subPage); while (matcherPocket.find()) { int maxCaches; try { maxCaches = Integer.parseInt(matcherPocket.group(1)); } catch (NumberFormatException e) { maxCaches = 0; Log.e("GCParser.searchPocketQueryList: Unable to parse max caches", e); } final String guid = Html.fromHtml(matcherPocket.group(2)).toString(); final String name = Html.fromHtml(matcherPocket.group(3)).toString(); final PocketQueryList pqList = new PocketQueryList(guid, name, maxCaches); list.add(pqList); } // just in case, lets sort the resulting list Collections.sort(list, new Comparator<PocketQueryList>() { @Override public int compare(PocketQueryList left, PocketQueryList right) { return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName()); } }); return list; } public static ImmutablePair<StatusCode, String> postLog(final String geocode, final String cacheid, final String[] viewstates, final LogType logType, final int year, final int month, final int day, final String log, final List<TrackableLog> trackables) { if (GCLogin.isEmpty(viewstates)) { Log.e("GCParser.postLog: No viewstate given"); return new ImmutablePair<StatusCode, String>(StatusCode.LOG_POST_ERROR, ""); } if (StringUtils.isBlank(log)) { Log.e("GCParser.postLog: No log text given"); return new ImmutablePair<StatusCode, String>(StatusCode.NO_LOG_TEXT, ""); } final String logInfo = log.replace("\n", "\r\n").trim(); // windows' eol and remove leading and trailing whitespaces Log.i("Trying to post log for cache #" + cacheid + " - action: " + logType + "; date: " + year + "." + month + "." + day + ", log: " + logInfo + "; trackables: " + (trackables != null ? trackables.size() : "0")); final Parameters params = new Parameters("__EVENTTARGET", "", "__EVENTARGUMENT", "", "__LASTFOCUS", "", "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id), "ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.getCustomGcDateFormat().format(new GregorianCalendar(year, month - 1, day).getTime()), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Month", Integer.toString(month), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Day", Integer.toString(day), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Year", Integer.toString(year), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged", String.format("%02d", month) + "/" + String.format("%02d", day) + "/" + String.format("%04d", year), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year), "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry", "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo, "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry", "ctl00$ContentBody$LogBookPanel1$uxLogCreationSource", "Old", "ctl00$ContentBody$uxVistOtherListingGC", ""); GCLogin.putViewstates(params, viewstates); if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed final StringBuilder hdnSelected = new StringBuilder(); for (final TrackableLog tb : trackables) { if (tb.action != LogTypeTrackable.DO_NOTHING) { hdnSelected.append(Integer.toString(tb.id)); hdnSelected.append(tb.action.action); hdnSelected.append(','); } } params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString(), // selected trackables "ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", ""); } final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/seek/log.aspx") .encodedQuery("ID=" + cacheid).build().toString(); String page = GCLogin.getInstance().postRequestLogged(uri, params); if (!GCLogin.getInstance().getLoginStatus(page)) { Log.e("GCParser.postLog: Cannot log in geocaching"); return new ImmutablePair<StatusCode, String>(StatusCode.NOT_LOGGED_IN, ""); } // maintenance, archived needs to be confirmed final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_MAINTENANCE, page); try { if (matcher.find() && matcher.groupCount() > 0) { final String[] viewstatesConfirm = GCLogin.getViewstates(page); if (GCLogin.isEmpty(viewstatesConfirm)) { Log.e("GCParser.postLog: No viewstate for confirm log"); return new ImmutablePair<StatusCode, String>(StatusCode.LOG_POST_ERROR, ""); } params.clear(); GCLogin.putViewstates(params, viewstatesConfirm); params.put("__EVENTTARGET", ""); params.put("__EVENTARGUMENT", ""); params.put("__LASTFOCUS", ""); params.put("ctl00$ContentBody$LogBookPanel1$btnConfirm", "Yes"); params.put("ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo); params.put("ctl00$ContentBody$uxVistOtherListingGC", ""); if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed final StringBuilder hdnSelected = new StringBuilder(); for (final TrackableLog tb : trackables) { final String action = Integer.toString(tb.id) + tb.action.action; final StringBuilder paramText = new StringBuilder( "ctl00$ContentBody$LogBookPanel1$uxTrackables$repTravelBugs$ctl"); if (tb.ctl < 10) { paramText.append('0'); } paramText.append(tb.ctl).append("$ddlAction"); params.put(paramText.toString(), action); if (tb.action != LogTypeTrackable.DO_NOTHING) { hdnSelected.append(action); hdnSelected.append(','); } } params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString()); // selected trackables params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", ""); } page = Network.getResponseData(Network.postRequest(uri, params)); } } catch (final RuntimeException e) { Log.e("GCParser.postLog.confim", e); } try { final MatcherWrapper matcherOk = new MatcherWrapper(GCConstants.PATTERN_OK1, page); if (matcherOk.find()) { Log.i("Log successfully posted to cache #" + cacheid); if (geocode != null) { DataStore.saveVisitDate(geocode); } GCLogin.getInstance().getLoginStatus(page); // the log-successful-page contains still the old value if (GCLogin.getInstance().getActualCachesFound() >= 0) { GCLogin.getInstance().setActualCachesFound(GCLogin.getInstance().getActualCachesFound() + 1); } final String logID = TextUtils.getMatch(page, GCConstants.PATTERN_LOG_IMAGE_UPLOAD, ""); return new ImmutablePair<StatusCode, String>(StatusCode.NO_ERROR, logID); } } catch (final Exception e) { Log.e("GCParser.postLog.check", e); } Log.e("GCParser.postLog: Failed to post log because of unknown error"); return new ImmutablePair<StatusCode, String>(StatusCode.LOG_POST_ERROR, ""); } /** * Upload an image to a log that has already been posted * * @param logId * the ID of the log to upload the image to. Found on page returned when log is uploaded * @param caption * of the image; max 50 chars * @param description * of the image; max 250 chars * @param imageUri * the URI for the image to be uploaded * @return status code to indicate success or failure */ public static ImmutablePair<StatusCode, String> uploadLogImage(final String logId, final String caption, final String description, final Uri imageUri) { final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com") .path("/seek/upload.aspx").encodedQuery("LID=" + logId).build().toString(); final String page = GCLogin.getInstance().getRequestLogged(uri, null); if (StringUtils.isBlank(page)) { Log.e("GCParser.uploadLogImage: No data from server"); return new ImmutablePair<StatusCode, String>(StatusCode.UNKNOWN_ERROR, null); } assert page != null; final String[] viewstates = GCLogin.getViewstates(page); final Parameters uploadParams = new Parameters("__EVENTTARGET", "", "__EVENTARGUMENT", "", "ctl00$ContentBody$ImageUploadControl1$uxFileCaption", caption, "ctl00$ContentBody$ImageUploadControl1$uxFileDesc", description, "ctl00$ContentBody$ImageUploadControl1$uxUpload", "Upload"); GCLogin.putViewstates(uploadParams, viewstates); final File image = new File(imageUri.getPath()); final String response = Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$ImageUploadControl1$uxFileUpload", "image/jpeg", image)); final MatcherWrapper matcherUrl = new MatcherWrapper(GCConstants.PATTERN_IMAGE_UPLOAD_URL, response); if (matcherUrl.find()) { Log.i("Logimage successfully uploaded."); final String uploadedImageUrl = matcherUrl.group(1); return ImmutablePair.of(StatusCode.NO_ERROR, uploadedImageUrl); } Log.e("GCParser.uploadLogIMage: Failed to upload image because of unknown error"); return ImmutablePair.of(StatusCode.LOGIMAGE_POST_ERROR, null); } /** * Post a log to GC.com. * * @return status code of the upload and ID of the log */ public static StatusCode postLogTrackable(final String tbid, final String trackingCode, final String[] viewstates, final LogType logType, final int year, final int month, final int day, final String log) { if (GCLogin.isEmpty(viewstates)) { Log.e("GCParser.postLogTrackable: No viewstate given"); return StatusCode.LOG_POST_ERROR; } if (StringUtils.isBlank(log)) { Log.e("GCParser.postLogTrackable: No log text given"); return StatusCode.NO_LOG_TEXT; } Log.i("Trying to post log for trackable #" + trackingCode + " - action: " + logType + "; date: " + year + "." + month + "." + day + ", log: " + log); final String logInfo = log.replace("\n", "\r\n"); // windows' eol final Calendar currentDate = Calendar.getInstance(); final Parameters params = new Parameters("__EVENTTARGET", "", "__EVENTARGUMENT", "", "__LASTFOCUS", "", "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id), "ctl00$ContentBody$LogBookPanel1$tbCode", trackingCode); GCLogin.putViewstates(params, viewstates); if (currentDate.get(Calendar.YEAR) == year && (currentDate.get(Calendar.MONTH) + 1) == month && currentDate.get(Calendar.DATE) == day) { params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged", ""); params.put("ctl00$ContentBody$LogBookPanel1$uxDateVisited", ""); } else { params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged", Integer.toString(month) + "/" + Integer.toString(day) + "/" + Integer.toString(year)); params.put("ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.getCustomGcDateFormat().format(new GregorianCalendar(year, month - 1, day).getTime())); } params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month), "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Day", Integer.toString(day), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Month", Integer.toString(month), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Year", Integer.toString(year), "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo, "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry", "ctl00$ContentBody$uxVistOtherTrackableTB", "", "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry", "ctl00$ContentBody$uxVistOtherListingGC", ""); final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/track/log.aspx") .encodedQuery("wid=" + tbid).build().toString(); final String page = GCLogin.getInstance().postRequestLogged(uri, params); if (!GCLogin.getInstance().getLoginStatus(page)) { Log.e("GCParser.postLogTrackable: Cannot log in geocaching"); return StatusCode.NOT_LOGGED_IN; } try { final MatcherWrapper matcherOk = new MatcherWrapper(GCConstants.PATTERN_OK2, page); if (matcherOk.find()) { Log.i("Log successfully posted to trackable #" + trackingCode); return StatusCode.NO_ERROR; } } catch (final Exception e) { Log.e("GCParser.postLogTrackable.check", e); } Log.e("GCParser.postLogTrackable: Failed to post log because of unknown error"); return StatusCode.LOG_POST_ERROR; } /** * Adds the cache to the watchlist of the user. * * @param cache * the cache to add * @return <code>false</code> if an error occurred, <code>true</code> otherwise */ static boolean addToWatchlist(final Geocache cache) { final String uri = "http://www.geocaching.com/my/watchlist.aspx?w=" + cache.getCacheId(); final String page = GCLogin.getInstance().postRequestLogged(uri, null); if (StringUtils.isBlank(page)) { Log.e("GCParser.addToWatchlist: No data from server"); return false; // error } final boolean guidOnPage = isGuidContainedInPage(cache, page); if (guidOnPage) { Log.i("GCParser.addToWatchlist: cache is on watchlist"); cache.setOnWatchlist(true); } else { Log.e("GCParser.addToWatchlist: cache is not on watchlist"); } return guidOnPage; // on watchlist (=added) / else: error } /** * Removes the cache from the watch list * * @param cache * the cache to remove * @return <code>false</code> if an error occurred, <code>true</code> otherwise */ static boolean removeFromWatchlist(final Geocache cache) { final String uri = "http://www.geocaching.com/my/watchlist.aspx?ds=1&action=rem&id=" + cache.getCacheId(); String page = GCLogin.getInstance().postRequestLogged(uri, null); if (StringUtils.isBlank(page)) { Log.e("GCParser.removeFromWatchlist: No data from server"); return false; // error } // removing cache from list needs approval by hitting "Yes" button final Parameters params = new Parameters("__EVENTTARGET", "", "__EVENTARGUMENT", "", "ctl00$ContentBody$btnYes", "Yes"); GCLogin.transferViewstates(page, params); page = Network.getResponseData(Network.postRequest(uri, params)); final boolean guidOnPage = isGuidContainedInPage(cache, page); if (!guidOnPage) { Log.i("GCParser.removeFromWatchlist: cache removed from watchlist"); cache.setOnWatchlist(false); } else { Log.e("GCParser.removeFromWatchlist: cache not removed from watchlist"); } return !guidOnPage; // on watch list (=error) / not on watch list } /** * Checks if a page contains the guid of a cache * * @param cache the geocache * @param page * the page to search in, may be null * @return true if the page contains the guid of the cache, false otherwise */ private static boolean isGuidContainedInPage(final Geocache cache, final String page) { if (StringUtils.isBlank(page) || StringUtils.isBlank(cache.getGuid())) { return false; } return Pattern.compile(cache.getGuid(), Pattern.CASE_INSENSITIVE).matcher(page).find(); } @Nullable static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log, final String numlogs) { final Parameters params = new Parameters("decrypt", "y"); if (StringUtils.isNotBlank(geocode)) { params.put("wp", geocode); } else if (StringUtils.isNotBlank(guid)) { params.put("guid", guid); } params.put("log", log); params.put("numlogs", numlogs); return GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/seek/cache_details.aspx", params); } /** * Adds the cache to the favorites of the user. * * This must not be called from the UI thread. * * @param cache * the cache to add * @return <code>false</code> if an error occurred, <code>true</code> otherwise */ static boolean addToFavorites(final Geocache cache) { return changeFavorite(cache, true); } private static boolean changeFavorite(final Geocache cache, final boolean add) { final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } final String uri = "http://www.geocaching.com/datastore/favorites.svc/update?u=" + userToken + "&f=" + Boolean.toString(add); final HttpResponse response = Network.postRequest(uri, null); if (response != null && response.getStatusLine().getStatusCode() == 200) { Log.i("GCParser.changeFavorite: cache added/removed to/from favorites"); cache.setFavorite(add); cache.setFavoritePoints(cache.getFavoritePoints() + (add ? 1 : -1)); return true; } Log.e("GCParser.changeFavorite: cache not added/removed to/from favorites"); return false; } private static String getUserToken(final Geocache cache) { final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); } /** * Removes the cache from the favorites. * * This must not be called from the UI thread. * * @param cache * the cache to remove * @return <code>false</code> if an error occurred, <code>true</code> otherwise */ static boolean removeFromFavorites(final Geocache cache) { return changeFavorite(cache, false); } /** * Parse a trackable HTML description into a Trackable object * * @param page * the HTML page to parse, already processed through {@link TextUtils#replaceWhitespace} * @return the parsed trackable, or null if none could be parsed */ static Trackable parseTrackable(final String page, final String possibleTrackingcode) { if (StringUtils.isBlank(page)) { Log.e("GCParser.parseTrackable: No page given"); return null; } if (page.contains(GCConstants.ERROR_TB_DOES_NOT_EXIST) || page.contains(GCConstants.ERROR_TB_ARITHMETIC_OVERFLOW) || page.contains(GCConstants.ERROR_TB_ELEMENT_EXCEPTION)) { return null; } final Trackable trackable = new Trackable(); // trackable geocode trackable.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GEOCODE, true, StringUtils.upperCase(possibleTrackingcode))); if (trackable.getGeocode() == null) { Log.e("GCParser.parseTrackable: could not figure out trackable geocode"); return null; } // trackable id trackable.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GUID, true, trackable.getGuid())); // trackable icon trackable.setIconUrl( TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ICON, true, trackable.getIconUrl())); // trackable name trackable.setName( Html.fromHtml(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_NAME, true, "")).toString()); // trackable type if (StringUtils.isNotBlank(trackable.getName())) { trackable.setType( TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_TYPE, true, trackable.getType())); } // trackable owner name try { final MatcherWrapper matcherOwner = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_OWNER, page); if (matcherOwner.find() && matcherOwner.groupCount() > 0) { trackable.setOwnerGuid(matcherOwner.group(1)); trackable.setOwner(matcherOwner.group(2).trim()); } } catch (final RuntimeException e) { // failed to parse trackable owner name Log.w("GCParser.parseTrackable: Failed to parse trackable owner name"); } // trackable origin trackable.setOrigin( TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ORIGIN, true, trackable.getOrigin())); // trackable spotted try { final MatcherWrapper matcherSpottedCache = new MatcherWrapper( GCConstants.PATTERN_TRACKABLE_SPOTTEDCACHE, page); if (matcherSpottedCache.find() && matcherSpottedCache.groupCount() > 0) { trackable.setSpottedGuid(matcherSpottedCache.group(1)); trackable.setSpottedName(matcherSpottedCache.group(2).trim()); trackable.setSpottedType(Trackable.SPOTTED_CACHE); } final MatcherWrapper matcherSpottedUser = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDUSER, page); if (matcherSpottedUser.find() && matcherSpottedUser.groupCount() > 0) { trackable.setSpottedGuid(matcherSpottedUser.group(1)); trackable.setSpottedName(matcherSpottedUser.group(2).trim()); trackable.setSpottedType(Trackable.SPOTTED_USER); } if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDUNKNOWN)) { trackable.setSpottedType(Trackable.SPOTTED_UNKNOWN); } if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDOWNER)) { trackable.setSpottedType(Trackable.SPOTTED_OWNER); } } catch (final RuntimeException e) { // failed to parse trackable last known place Log.w("GCParser.parseTrackable: Failed to parse trackable last known place"); } // released date - can be missing on the page final String releaseString = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_RELEASES, false, null); if (releaseString != null) { try { trackable.setReleased(dateTbIn1.parse(releaseString)); } catch (ParseException e) { if (trackable.getReleased() == null) { try { trackable.setReleased(dateTbIn2.parse(releaseString)); } catch (ParseException e1) { Log.e("Could not parse trackable release " + releaseString); } } } } // trackable distance final String distance = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_DISTANCE, false, null); if (null != distance) { try { trackable.setDistance(DistanceParser.parseDistance(distance, !Settings.isUseImperialUnits())); } catch (final NumberFormatException e) { Log.e("GCParser.parseTrackable: Failed to parse distance", e); } } // trackable goal trackable.setGoal(convertLinks( TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GOAL, true, trackable.getGoal()))); // trackable details & image try { final MatcherWrapper matcherDetailsImage = new MatcherWrapper( GCConstants.PATTERN_TRACKABLE_DETAILSIMAGE, page); if (matcherDetailsImage.find() && matcherDetailsImage.groupCount() > 0) { final String image = StringUtils.trim(matcherDetailsImage.group(3)); final String details = StringUtils.trim(matcherDetailsImage.group(4)); if (StringUtils.isNotEmpty(image)) { trackable.setImage(image); } if (StringUtils.isNotEmpty(details) && !StringUtils.equals(details, "No additional details available.")) { trackable.setDetails(convertLinks(details)); } } } catch (final RuntimeException e) { // failed to parse trackable details & image Log.w("GCParser.parseTrackable: Failed to parse trackable details & image"); } if (StringUtils.isEmpty(trackable.getDetails()) && page.contains(GCConstants.ERROR_TB_NOT_ACTIVATED)) { trackable.setDetails(CgeoApplication.getInstance().getString(R.string.trackable_not_activated)); } // trackable logs try { final MatcherWrapper matcherLogs = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG, page); /* * 1. Type (image) * 2. Date * 3. Author * 4. Cache-GUID * 5. <ignored> (strike-through property for ancient caches) * 6. Cache-name * 7. Log text */ while (matcherLogs.find()) { long date = 0; try { date = GCLogin.parseGcCustomDate(matcherLogs.group(2)).getTime(); } catch (final ParseException e) { } final LogEntry logDone = new LogEntry(Html.fromHtml(matcherLogs.group(3)).toString().trim(), date, LogType.getByIconName(matcherLogs.group(1)), matcherLogs.group(7).trim()); if (matcherLogs.group(4) != null && matcherLogs.group(6) != null) { logDone.cacheGuid = matcherLogs.group(4); logDone.cacheName = matcherLogs.group(6); } // Apply the pattern for images in a trackable log entry against each full log (group(0)) final String logEntry = matcherLogs.group(0); final MatcherWrapper matcherLogImages = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG_IMAGES, logEntry); /* * 1. Image URL * 2. Image title */ while (matcherLogImages.find()) { final Image logImage = new Image(matcherLogImages.group(1), matcherLogImages.group(2)); logDone.addLogImage(logImage); } trackable.getLogs().add(logDone); } } catch (final Exception e) { // failed to parse logs Log.w("GCParser.parseCache: Failed to parse cache logs", e); } // tracking code if (!StringUtils.equalsIgnoreCase(trackable.getGeocode(), possibleTrackingcode)) { trackable.setTrackingcode(possibleTrackingcode); } if (CgeoApplication.getInstance() != null) { DataStore.saveTrackable(trackable); } return trackable; } private static String convertLinks(String input) { if (input == null) { return null; } return StringUtils.replace(input, "../", GCConstants.GC_URL); } /** * Extract logs from a cache details page. * * @param page * the text of the details page * @param friends * return friends logs only (will require a network request) * @return a list of log entries or <code>null</code> if the logs could not be retrieved * */ @Nullable private static List<LogEntry> getLogsFromDetails(final String page, final boolean friends) { String rawResponse; if (friends) { final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); if (!userTokenMatcher.find()) { Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); return null; } final String userToken = userTokenMatcher.group(1); final Parameters params = new Parameters("tkn", userToken, "idx", "1", "num", String.valueOf(GCConstants.NUMBER_OF_LOGS), "decrypt", "true", // "sp", Boolean.toString(personal), // personal logs "sf", Boolean.toString(friends)); final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params); if (response == null) { Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null"); return null; } final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information"); return null; } rawResponse = Network.getResponseData(response); if (rawResponse == null) { Log.e("GCParser.loadLogsFromDetails: unable to read whole response"); return null; } } else { // extract embedded JSON data from page rawResponse = TextUtils.getMatch(page, GCConstants.PATTERN_LOGBOOK, ""); } return parseLogs(friends, rawResponse); } private static List<LogEntry> parseLogs(final boolean friends, String rawResponse) { final List<LogEntry> logs = new ArrayList<LogEntry>(); // for non logged in users the log book is not shown if (StringUtils.isBlank(rawResponse)) { return logs; } try { final JSONObject resp = new JSONObject(rawResponse); if (!resp.getString("status").equals("success")) { Log.e("GCParser.loadLogsFromDetails: status is " + resp.getString("status")); return null; } final JSONArray data = resp.getJSONArray("data"); for (int index = 0; index < data.length(); index++) { final JSONObject entry = data.getJSONObject(index); // FIXME: use the "LogType" field instead of the "LogTypeImage" one. final String logIconNameExt = entry.optString("LogTypeImage", ".gif"); final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4); long date = 0; try { date = GCLogin.parseGcCustomDate(entry.getString("Visited")).getTime(); } catch (final ParseException e) { Log.e("GCParser.loadLogsFromDetails: failed to parse log date."); } // TODO: we should update our log data structure to be able to record // proper coordinates, and make them clickable. In the meantime, it is // better to integrate those coordinates into the text rather than not // display them at all. final String latLon = entry.getString("LatLonString"); final String logText = (StringUtils.isEmpty(latLon) ? "" : (latLon + "<br/><br/>")) + TextUtils.removeControlCharacters(entry.getString("LogText")); final LogEntry logDone = new LogEntry( TextUtils.removeControlCharacters(entry.getString("UserName")), date, LogType.getByIconName(logIconName), logText); logDone.found = entry.getInt("GeocacheFindCount"); logDone.friend = friends; final JSONArray images = entry.getJSONArray("Images"); for (int i = 0; i < images.length(); i++) { final JSONObject image = images.getJSONObject(i); final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.getString("FileName"); final String title = TextUtils.removeControlCharacters(image.getString("Name")); final Image logImage = new Image(url, title); logDone.addLogImage(logImage); } logs.add(logDone); } } catch (final JSONException e) { // failed to parse logs Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e); } return logs; } @NonNull public static List<LogType> parseTypes(String page) { if (StringUtils.isEmpty(page)) { return Collections.emptyList(); } final List<LogType> types = new ArrayList<LogType>(); final MatcherWrapper typeBoxMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPEBOX, page); if (typeBoxMatcher.find() && typeBoxMatcher.groupCount() > 0) { final String typesText = typeBoxMatcher.group(1); final MatcherWrapper typeMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPE2, typesText); while (typeMatcher.find()) { if (typeMatcher.groupCount() > 1) { try { final int type = Integer.parseInt(typeMatcher.group(2)); if (type > 0) { types.add(LogType.getById(type)); } } catch (final NumberFormatException e) { Log.e("Error parsing log types", e); } } } } // we don't support this log type types.remove(LogType.UPDATE_COORDINATES); return types; } public static List<TrackableLog> parseTrackableLog(final String page) { if (StringUtils.isEmpty(page)) { return null; } String table = StringUtils.substringBetween(page, "<table id=\"tblTravelBugs\"", "</table>"); // if no trackables are currently in the account, the table is not available, so return an empty list instead of null if (StringUtils.isBlank(table)) { return Collections.emptyList(); } table = StringUtils.substringBetween(table, "<tbody>", "</tbody>"); if (StringUtils.isBlank(table)) { Log.e("GCParser.parseTrackableLog: tbody not found on page"); return null; } final List<TrackableLog> trackableLogs = new ArrayList<TrackableLog>(); final MatcherWrapper trackableMatcher = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE, page); while (trackableMatcher.find()) { if (trackableMatcher.groupCount() > 0) { final String trackCode = trackableMatcher.group(1); final String name = Html.fromHtml(trackableMatcher.group(2)).toString(); try { final Integer ctl = Integer.valueOf(trackableMatcher.group(3)); final Integer id = Integer.valueOf(trackableMatcher.group(5)); if (trackCode != null && ctl != null && id != null) { final TrackableLog entry = new TrackableLog(trackCode, name, id, ctl); Log.i("Trackable in inventory (#" + entry.ctl + "/" + entry.id + "): " + entry.trackCode + " - " + entry.name); trackableLogs.add(entry); } } catch (final NumberFormatException e) { Log.e("GCParser.parseTrackableLog", e); } } } return trackableLogs; } /** * Insert the right cache type restriction in parameters * * @param params * the parameters to insert the restriction into * @param cacheType * the type of cache, or null to include everything */ static private void insertCacheType(final Parameters params, final CacheType cacheType) { params.put("tx", cacheType.guid); } private static void getExtraOnlineInfo(final Geocache cache, final String page, final CancellableHandler handler) { if (CancellableHandler.isCancelled(handler)) { return; } //cache.setLogs(loadLogsFromDetails(page, cache, false)); if (Settings.isFriendLogsWanted()) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); final List<LogEntry> allLogs = cache.getLogs(); final List<LogEntry> friendLogs = getLogsFromDetails(page, true); if (friendLogs != null) { for (final LogEntry log : friendLogs) { if (allLogs.contains(log)) { allLogs.get(allLogs.indexOf(log)).friend = true; } else { cache.getLogs().add(log); } } } } if (Settings.isRatingWanted()) { if (CancellableHandler.isCancelled(handler)) { return; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_gcvote); final GCVoteRating rating = GCVote.getRating(cache.getGuid(), cache.getGeocode()); if (rating != null) { cache.setRating(rating.getRating()); cache.setVotes(rating.getVotes()); cache.setMyVote(rating.getMyVote()); } } } public static boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) { return editModifiedCoordinates(cache, wpt); } public static boolean deleteModifiedCoordinates(Geocache cache) { return editModifiedCoordinates(cache, null); } public static boolean editModifiedCoordinates(Geocache cache, Geopoint wpt) { final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } try { JSONObject jo; if (wpt != null) { jo = new JSONObject().put("dto", (new JSONObject().put("ut", userToken).put("data", new JSONObject() .put("lat", wpt.getLatitudeE6() / 1E6).put("lng", wpt.getLongitudeE6() / 1E6)))); } else { jo = new JSONObject().put("dto", (new JSONObject().put("ut", userToken))); } final String uriSuffix = wpt != null ? "SetUserCoordinate" : "ResetUserCoordinate"; final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); Log.i("Sending to " + uriPrefix + uriSuffix + " :" + jo.toString()); if (response != null && response.getStatusLine().getStatusCode() == 200) { Log.i("GCParser.editModifiedCoordinates - edited on GC.com"); return true; } } catch (final JSONException e) { Log.e("Unknown exception with json wrap code", e); } Log.e("GCParser.deleteModifiedCoordinates - cannot delete modified coords"); return false; } public static boolean uploadPersonalNote(Geocache cache) { final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } try { final JSONObject jo = new JSONObject().put("dto", (new JSONObject().put("et", cache.getPersonalNote()).put("ut", userToken))); final String uriSuffix = "SetUserCacheNote"; final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); Log.i("Sending to " + uriPrefix + uriSuffix + " :" + jo.toString()); if (response != null && response.getStatusLine().getStatusCode() == 200) { Log.i("GCParser.uploadPersonalNote - uploaded to GC.com"); return true; } } catch (final JSONException e) { Log.e("Unknown exception with json wrap code", e); } Log.e("GCParser.uploadPersonalNote - cannot upload personal note"); return false; } }