package cgeo.geocaching.connector.su; import android.support.annotation.NonNull; import org.apache.commons.lang3.StringUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Locale; import cgeo.geocaching.SearchResult; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.log.LogEntry; import cgeo.geocaching.log.LogType; import cgeo.geocaching.log.LogEntry.Builder; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Image; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.SynchronizedDateFormat; public class GeocachingSuParser { private static final SynchronizedDateFormat DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd", Locale.US); private static final SynchronizedDateFormat DATE_TIME_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); private GeocachingSuParser() { // utility class } /** * Collects temporary data until parsing of a single cache is completed, since not all parsed tags or attributes can * be stored immediately. */ private static final class Parsed { public String id; private final StringBuilder description = new StringBuilder(); public String latitude = null; public Builder logBuilder = new LogEntry.Builder(); public final List<LogEntry> logs = new ArrayList<>(); public String type; void addDescription(final String text) { if (StringUtils.isBlank(text)) { return; } if (description.length() > 0) { description.append('\n'); } description.append(StringUtils.trim(text)); } String getDescription() { return description.toString(); } } @NonNull public static SearchResult parseCaches(final String endTag, final InputStream inputStream) { final ArrayList<Geocache> caches = new ArrayList<>(); try { final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); final XmlPullParser parser = factory.newPullParser(); parser.setInput(inputStream, "UTF-8"); Parsed parsed = new Parsed(); Geocache cache = createNewCache(); String text = ""; int eventType = parser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { final String tagname = parser.getName(); switch (eventType) { case XmlPullParser.START_TAG: // reset text value to correctly indicate empty tags text = ""; if ("cache".equalsIgnoreCase(tagname)) { cache = createNewCache(); parsed = new Parsed(); } else if ("note".equalsIgnoreCase(tagname)) { parsed.logBuilder = new LogEntry.Builder(); parsed.logBuilder.setAuthor(parser.getAttributeValue(null, "nick")); parsed.logBuilder.setDate(parseDateTime(parser.getAttributeValue(null, "date"))); parsed.logBuilder.setLogType(parseLogType(parser.getAttributeValue(null, "status"))); } break; case XmlPullParser.TEXT: text = parser.getText(); break; case XmlPullParser.END_TAG: if ("id".equalsIgnoreCase(tagname)) { parsed.id = text; } else if ("name".equalsIgnoreCase(tagname)) { cache.setName(text); } else if (endTag.equalsIgnoreCase(tagname)) { storeCache(cache, caches, parsed); } else if ("lat".equalsIgnoreCase(tagname)) { parsed.latitude = text; } else if ("lng".equalsIgnoreCase(tagname)) { cache.setCoords(new Geopoint(parsed.latitude, text)); } else if ("nick".equalsIgnoreCase(tagname) || "autor".equalsIgnoreCase(tagname)) { // sic!, "autor", not "author" cache.setOwnerDisplayName(text); } else if ("adesc".equalsIgnoreCase(tagname)) { // description of the area parsed.addDescription(text); } else if ("cdesc".equalsIgnoreCase(tagname)) { // description of the cache task parsed.addDescription(text); } else if ("tpart".equalsIgnoreCase(tagname)) { // description for traditional part (optional, rarely used), where to look for the cache parsed.addDescription(text); } else if ("vpart".equalsIgnoreCase(tagname)) { // virtual question for winter time (or just virtual question for virtual caches) parsed.addDescription(text); } else if ("date".equalsIgnoreCase(tagname)) { cache.setHidden(parseDate(text)); } else if ("type".equalsIgnoreCase(tagname) || "ctype".equalsIgnoreCase(tagname)) { // different tags used in single cache details and area search parsed.type = text; cache.setType(parseType(text)); } else if ("note".equalsIgnoreCase(tagname)) { parsed.logBuilder.setLog(StringUtils.trim(text)); parsed.logs.add(parsed.logBuilder.build()); } else if ("img".equalsIgnoreCase(tagname)) { if (text.contains("photos/caches")) { cache.addSpoiler(new Image.Builder().setUrl(text).build()); } else { parsed.addDescription("<img src=\"" + text + "\"/><br/>"); } } else if ("status".equalsIgnoreCase(tagname)) { cache.setDisabled(isDisabledStatus(text)); } else if ("recom".equalsIgnoreCase(tagname)) { cache.setFavoritePoints(Integer.parseInt(StringUtils.trim(text))); } else if ("rating".equalsIgnoreCase(tagname)) { final String trimmed = StringUtils.trim(text); if (StringUtils.isNotEmpty(trimmed)) { cache.setRating(Float.valueOf(trimmed)); } } else if ("container".equalsIgnoreCase(tagname)) { // we only have the geocaching.com container sizes, therefore let's move this into the hint final String trimmed = StringUtils.trim(text); if (StringUtils.isNotEmpty(trimmed)) { cache.setHint(trimmed); } } else if ("area_value".equalsIgnoreCase(tagname)) { cache.setTerrain(Float.parseFloat(text)); } else if ("cache_value".equalsIgnoreCase(tagname)) { cache.setDifficulty(Float.parseFloat(text)); } break; default: break; } eventType = parser.next(); } } catch (XmlPullParserException | IOException | ParseException e) { Log.e("Error parsing geocaching.su data", e); } return new SearchResult(caches); } private static LogType parseLogType(final String status) { switch (status) { case "1": return LogType.FOUND_IT; case "2": return LogType.DIDNT_FIND_IT; case "3": return LogType.NOTE; case "4": return LogType.DIDNT_FIND_IT; case "5": return LogType.OWNER_MAINTENANCE; case "6": return LogType.OWNER_MAINTENANCE; default: return LogType.UNKNOWN; } } private static boolean isDisabledStatus(final String status) { return !("1".equals(status) || "На сайте".equalsIgnoreCase(status)); } private static void storeCache(final Geocache cache, final ArrayList<Geocache> caches, final Parsed parsed) { // finalize the data of the cache cache.setGeocode(getGeocode(parsed)); final String description = parsed.getDescription(); cache.setDescription(description); // differentiate between area search, and detailed request if (StringUtils.isNotEmpty(description)) { cache.setDetailedUpdatedNow(); DataStore.saveLogs(cache.getGeocode(), parsed.logs); } // save to database DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); // append to search result caches.add(cache); } private static String getGeocode(final Parsed parsed) { return parseGeocodePrefix(parsed.type) + parsed.id; } private static CharSequence parseGeocodePrefix(final String type) { switch (type) { case "Пошаговый виртуальный": return GeocachingSuConnector.PREFIX_MULTISTEP_VIRTUAL; case "Традиционный": return GeocachingSuConnector.PREFIX_TRADITIONAL; case "Виртуальный": return GeocachingSuConnector.PREFIX_VIRTUAL; case "Сообщение о встрече": return GeocachingSuConnector.PREFIX_EVENT; case "Пошаговый традиционный": return GeocachingSuConnector.PREFIX_MULTISTEP; case "Конкурс": return GeocachingSuConnector.PREFIX_CONTEST; default: return "SU"; // fallback solution to not use the numeric id only } } @NonNull private static Geocache createNewCache() { final Geocache cache = new Geocache(); cache.setReliableLatLon(true); cache.setDetailed(false); return cache; } @NonNull private static CacheType parseType(final String text) { if (text.equalsIgnoreCase("Традиционный")) { return CacheType.TRADITIONAL; } if (text.equalsIgnoreCase("Виртуальный")) { return CacheType.VIRTUAL; } if (text.equalsIgnoreCase("Сообщение о встрече")) { return CacheType.EVENT; } if (text.equalsIgnoreCase("Конкурс")) { return CacheType.EVENT; } if (text.equalsIgnoreCase("Пошаговый традиционный")) { return CacheType.MULTI; } if (text.equalsIgnoreCase("Пошаговый виртуальный")) { return CacheType.VIRTUAL; } return CacheType.UNKNOWN; } private static Date parseDate(final String text) throws ParseException { return DATE_FORMAT.parse(text); } private static long parseDateTime(final String text) throws ParseException { return DATE_TIME_FORMAT.parse(text).getTime(); } }