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.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GEOCODE, true, StringUtils.upperCase(possibleTrackingcode)));
        trackable.forceSetBrand(TrackableBrand.TRAVELBUG);
        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
        final String iconUrl = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ICON, true, trackable.getIconUrl());
        trackable.setIconUrl(iconUrl.startsWith("/") ? "https://www.geocaching.com" + iconUrl : iconUrl);

        // trackable name
        trackable.setName(TextUtils.stripHtml(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_NAME, true, "")));

        // trackable type
        if (StringUtils.isNotBlank(trackable.getName())) {
            // old TB pages include TB type as "alt" attribute
            String type = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_TYPE, true, trackable.getType());
            if (StringUtils.isNotBlank(type)) {
                type = TextUtils.stripHtml(type);
            } else {
                // try alternative way on pages formatted the newer style: <title>\n\t(TBxxxx) Type - Name\n</title>
                final String title = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_TYPE_TITLE, true, "");
                if (StringUtils.isNotBlank(title)) {
                    final String nameWithHTML = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_NAME, true, "");
                    final int pos = StringUtils.lastIndexOfIgnoreCase(title, nameWithHTML);
                    if (pos != INDEX_NOT_FOUND) {
                        type = substring(title, 0, pos - 3);
                        type = TextUtils.stripHtml(type);
                    }
                }
            }
            trackable.setType(type);
        }

        // trackable owner name
        try {
            final MatcherWrapper matcherOwner = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_OWNER, page);
            if (matcherOwner.find()) {
                trackable.setOwnerGuid(matcherOwner.group(2));
                trackable.setOwner(matcherOwner.group(3).trim());

            }
        } catch (final RuntimeException e) {
            // failed to parse trackable owner name
            Log.w("GCParser.parseTrackable: Failed to parse trackable owner name", e);
        }

        // 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_BY_GEOCODE, page);
            if (matcherSpottedCache.find()) {
                trackable.setSpottedCacheGeocode(matcherSpottedCache.group(2));
                trackable.setSpottedName(matcherSpottedCache.group(1).trim());
                trackable.setSpottedType(Trackable.SPOTTED_CACHE);
            } else {
                final MatcherWrapper matcherSpottedCacheGuid = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDCACHE_BY_GUID, page);
                if (matcherSpottedCacheGuid.find()) {
                    trackable.setSpottedGuid(matcherSpottedCache.group(2));
                    trackable.setSpottedName(matcherSpottedCache.group(1).trim());
                    trackable.setSpottedType(Trackable.SPOTTED_CACHE);
                }
            }

            final MatcherWrapper matcherSpottedUser = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDUSER, page);
            if (matcherSpottedUser.find()) {
                trackable.setSpottedGuid(matcherSpottedUser.group(3));
                trackable.setSpottedName(HtmlCompat.fromHtml(matcherSpottedUser.group(1), HtmlCompat.FROM_HTML_MODE_LEGACY).toString().trim()); // remove HTML in parsed username, see #14442
                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", e);
        }

        // 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(DATE_TB_IN_1.parse(releaseString));
            } catch (final ParseException ignored) {
                if (trackable.getReleased() == null) {
                    try {
                        trackable.setReleased(DATE_TB_IN_2.parse(releaseString));
                    } catch (final ParseException e) {
                        Log.e("Could not parse trackable release " + releaseString, e);
                    }
                }
            }
        }

        // log - entire section can be missing on the page if trackable hasn't been found by the user
        try {
            final String logType = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_FOUND_LOG, false, null);
            if (logType != null) {
                trackable.setLogType(LogType.getByIconName(StringUtils.trim(logType)));
            }
            final MatcherWrapper retrievedMatcher = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_DISPOSITION_LOG, page);
            if (retrievedMatcher.find()) {
                trackable.setLogDate(GCLogin.parseGcCustomDate(StringUtils.trim(retrievedMatcher.group(2))));
                trackable.setLogGuid(StringUtils.trim(retrievedMatcher.group(1)));
            }
        } catch (final Exception e) {
            Log.e("GCParser.parseTrackable: Failed to parse log", e);
        }

        // trackable distance
        final MatcherWrapper distanceMatcher = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_DISTANCE, page);
        if (distanceMatcher.find()) {
            final DistanceUnit unit = DistanceUnit.findById(distanceMatcher.group(2),
                    Settings.useImperialUnits() ? DistanceUnit.MILE : DistanceUnit.KILOMETER);
            try {
                trackable.setDistance(unit.parseToKilometers(distanceMatcher.group(1)));
            } 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()) {
                final String image = StringUtils.trim(matcherDetailsImage.group(3));
                final String details = StringUtils.trim(matcherDetailsImage.group(4));

                if (StringUtils.isNotEmpty(image)) {
                    trackable.setImage(StringUtils.replace(image, "/display/", "/large/"));
                }
                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", e);
        }
        if (StringUtils.isEmpty(trackable.getDetails()) && page.contains(GCConstants.ERROR_TB_NOT_ACTIVATED)) {
            trackable.setDetails(CgeoApplication.getInstance().getString(R.string.trackable_not_activated));
        }

        // trackable may be locked (see e.g. TB673CE)
        if (new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_IS_LOCKED, page).find()) {
            trackable.setIsLocked();
        }

        // trackable logs
        try {
            final MatcherWrapper matcherLogsOuter = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG_OUTER, page);
            /*
             * 1. Type (image)
             * 2. Date
             * 3. Author-GUID
             * 4. Author
             * 5. Cache-GUID or cache-code
             * 6. <ignored> (strike-through property for ancient caches)
             * 7. Cache-name
             * 8. Log-ID
             * 9. Log text
             */
            while (matcherLogsOuter.find()) {
                // search each log block separately
                final MatcherWrapper matcherLogs = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG_INNER, matcherLogsOuter.group(0));
                while (matcherLogs.find()) {
                    long date = 0;
                    try {
                        date = GCLogin.parseGcCustomDate(matcherLogs.group(2)).getTime();
                    } catch (final ParseException ignored) {
                    }

                    final LogEntry.Builder logDoneBuilder = new LogEntry.Builder()
                            .setAuthor(TextUtils.stripHtml(matcherLogs.group(4)).trim())
                            .setAuthorGuid(matcherLogs.group(3))
                            .setDate(date)
                            .setLogType(LogType.getByIconName(matcherLogs.group(1)))
                            .setServiceLogId(matcherLogs.group(8))
                            .setLog(matcherLogs.group(9).trim());

                    if (matcherLogs.group(5) != null && matcherLogs.group(7) != null) {
                        logDoneBuilder.setCacheGeocode(matcherLogs.group(5));
                        logDoneBuilder.setCacheName(matcherLogs.group(7));
                    }

                    // Apply the pattern for images in a trackable log entry against each full log (group(0))
                    final String logEntry = matcherLogsOuter.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.Builder()
                                .setUrl(matcherLogImages.group(1))
                                .setTitle(matcherLogImages.group(2))
                                .setCategory(Image.ImageCategory.LOG)
                                .build();
                        logDoneBuilder.addLogImage(logImage);
                    }

                    trackable.getLogs().add(logDoneBuilder.build());
                }
            }
        } 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;
    }
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
main/src/main/java/cgeo/geocaching/connector/gc/GCParser.java