package cgeo.geocaching.files; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.connector.tc.TerraCachingLogType; import cgeo.geocaching.connector.tc.TerraCachingType; import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.LoadFlag; import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.log.LogEntry; import cgeo.geocaching.log.LogType; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.models.Waypoint; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.utils.DisposableHandler; import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; import cgeo.geocaching.utils.SynchronizedDateFormat; import android.sax.Element; import android.sax.EndElementListener; import android.sax.EndTextElementListener; import android.sax.RootElement; import android.sax.StartElementListener; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Xml; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.xml.sax.Attributes; import org.xml.sax.SAXException; abstract class GPXParser extends FileParser { private static final SynchronizedDateFormat formatSimple = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); // 2010-04-20T07:00:00 private static final SynchronizedDateFormat formatSimpleZ = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); // 2010-04-20T07:00:00Z private static final SynchronizedDateFormat formatTimezone = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); // 2010-04-20T01:01:03-04:00 /** * Attention: case sensitive geocode pattern to avoid matching normal words in the name or description of the cache. */ private static final Pattern PATTERN_GEOCODE = Pattern.compile("[0-9A-Z]{5,}"); private static final Pattern PATTERN_GUID = Pattern.compile(".*" + Pattern.quote("guid=") + "([0-9a-z\\-]+)", Pattern.CASE_INSENSITIVE); private static final Pattern PATTERN_URL_GEOCODE = Pattern.compile(".*" + Pattern.quote("wp=") + "([A-Z][0-9A-Z]+)", Pattern.CASE_INSENSITIVE); /** * supported groundspeak extensions of the GPX format */ private static final String[] GROUNDSPEAK_NAMESPACE = { "http://www.groundspeak.com/cache/1/1", // PQ 1.1 "http://www.groundspeak.com/cache/1/0/1", // PQ 1.0.1 "http://www.groundspeak.com/cache/1/0", // PQ 1.0 }; /** * supported GSAK extension of the GPX format */ private static final String[] GSAK_NS = { "http://www.gsak.net/xmlv1/1", "http://www.gsak.net/xmlv1/2", "http://www.gsak.net/xmlv1/3", "http://www.gsak.net/xmlv1/4", "http://www.gsak.net/xmlv1/5", "http://www.gsak.net/xmlv1/6" }; /** * c:geo extensions of the gpx format */ private static final String CGEO_NS = "http://www.cgeo.org/wptext/1/0"; private static final Pattern PATTERN_MILLISECONDS = Pattern.compile("\\.\\d{3,7}"); private int listId = StoredList.STANDARD_LIST_ID; protected final String namespace; private final String version; private Geocache cache; private Trackable trackable = new Trackable(); private LogEntry.Builder logBuilder = null; private String type = null; private String sym = null; private String name = null; private String cmt = null; private String desc = null; protected final String[] userData = new String[5]; // take 5 cells, that makes indexing 1..4 easier private String parentCacheCode = null; private boolean wptVisited = false; private boolean wptUserDefined = false; private List<LogEntry> logs = new ArrayList<>(); /** * Parser result. Maps geocode to cache. */ private final Set<String> result = new HashSet<>(100); private ProgressInputStream progressStream; /** * URL contained in the header of the GPX file. Used to guess where the file is coming from. */ protected String scriptUrl; /** * original longitude in case of modified coordinates */ @Nullable protected String originalLon; /** * original latitude in case of modified coordinates */ @Nullable protected String originalLat; /** * Unfortunately we can only detect terracaching child waypoints by remembering the state of the parent */ private boolean terraChildWaypoint = false; private final class UserDataListener implements EndTextElementListener { private final int index; UserDataListener(final int index) { this.index = index; } @Override public void end(final String user) { userData[index] = validate(user); } } protected GPXParser(final int listIdIn, final String namespaceIn, final String versionIn) { listId = listIdIn; namespace = namespaceIn; version = versionIn; } static Date parseDate(final String inputUntrimmed) throws ParseException { // remove milliseconds to reduce number of needed patterns final MatcherWrapper matcher = new MatcherWrapper(PATTERN_MILLISECONDS, inputUntrimmed.trim()); final String input = matcher.replaceFirst(""); if (input.contains("Z")) { return formatSimpleZ.parse(input); } if (StringUtils.countMatches(input, ":") == 3) { final String removeColon = input.substring(0, input.length() - 3) + input.substring(input.length() - 2); return formatTimezone.parse(removeColon); } return formatSimple.parse(input); } @Override @NonNull public Collection<Geocache> parse(@NonNull final InputStream stream, @Nullable final DisposableHandler progressHandler) throws IOException, ParserException { // when importing a ZIP, reset the child waypoint state terraChildWaypoint = false; resetCache(); final RootElement root = new RootElement(namespace, "gpx"); final Element waypoint = root.getChild(namespace, "wpt"); root.getChild(namespace, "url").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { scriptUrl = body; } }); root.getChild(namespace, "creator").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { scriptUrl = body; } }); // waypoint - attributes waypoint.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { try { if (attrs.getIndex("lat") > -1 && attrs.getIndex("lon") > -1) { final String latitude = attrs.getValue("lat"); final String longitude = attrs.getValue("lon"); // latitude and longitude are required attributes, but we export them empty for waypoints without coordinates if (StringUtils.isNotBlank(latitude) && StringUtils.isNotBlank(longitude)) { cache.setCoords(new Geopoint(Double.parseDouble(latitude), Double.parseDouble(longitude))); } } } catch (final NumberFormatException e) { Log.w("Failed to parse waypoint's latitude and/or longitude", e); } } }); // waypoint waypoint.setEndElementListener(new EndElementListener() { @Override public void end() { // try to find geocode somewhere else if (StringUtils.isBlank(cache.getGeocode())) { findGeoCode(name); findGeoCode(desc); findGeoCode(cmt); } // take the name as code, if nothing else is available if (StringUtils.isBlank(cache.getGeocode()) && StringUtils.isNotBlank(name)) { cache.setGeocode(name.trim()); } if (isValidForImport()) { fixCache(cache); if (listId != StoredList.TEMPORARY_LIST.id) { cache.getLists().add(listId); } cache.setDetailed(true); createNoteFromGSAKUserdata(); final String geocode = cache.getGeocode(); if (result.contains(geocode)) { Log.w("Duplicate geocode during GPX import: " + geocode); } // modify cache depending on the use case/connector afterParsing(cache); // finally store the cache in the database result.add(geocode); DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); DataStore.saveLogs(cache.getGeocode(), logs); // avoid the cachecache using lots of memory for caches which the user did not actually look at DataStore.removeCache(geocode, EnumSet.of(RemoveFlag.CACHE)); showProgressMessage(progressHandler, progressStream.getProgress()); } else if (StringUtils.isNotBlank(cache.getName()) && (StringUtils.containsIgnoreCase(type, "waypoint") || terraChildWaypoint)) { addWaypointToCache(); } resetCache(); } private void addWaypointToCache() { fixCache(cache); if (cache.getName().length() > 2 || StringUtils.isNotBlank(parentCacheCode)) { if (StringUtils.isBlank(parentCacheCode)) { if (StringUtils.containsIgnoreCase(scriptUrl, "extremcaching")) { parentCacheCode = cache.getName().substring(2); } else if (terraChildWaypoint) { parentCacheCode = StringUtils.left(cache.getGeocode(), cache.getGeocode().length() - 1); } else { parentCacheCode = "GC" + cache.getName().substring(2).toUpperCase(Locale.US); } } if ("GC_WayPoint1".equals(cache.getShortDescription())) { cache.setShortDescription(""); } final Geocache cacheForWaypoint = findParentCache(); if (cacheForWaypoint != null) { final Waypoint waypoint = new Waypoint(cache.getShortDescription(), WaypointType.fromGPXString(sym), false); if (wptUserDefined) { waypoint.setUserDefined(); } waypoint.setId(-1); waypoint.setGeocode(parentCacheCode); waypoint.setPrefix(cacheForWaypoint.getWaypointPrefix(cache.getName())); waypoint.setLookup("---"); // there is no lookup code in gpx file waypoint.setCoords(cache.getCoords()); waypoint.setNote(cache.getDescription()); waypoint.setVisited(wptVisited); final List<Waypoint> mergedWayPoints = new ArrayList<>(cacheForWaypoint.getWaypoints()); final List<Waypoint> newPoints = new ArrayList<>(); newPoints.add(waypoint); Waypoint.mergeWayPoints(newPoints, mergedWayPoints, true); cacheForWaypoint.setWaypoints(newPoints, false); DataStore.saveCache(cacheForWaypoint, EnumSet.of(SaveFlag.DB)); showProgressMessage(progressHandler, progressStream.getProgress()); } } } }); // waypoint.time waypoint.getChild(namespace, "time").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { try { cache.setHidden(parseDate(body)); } catch (final Exception e) { Log.w("Failed to parse cache date", e); } } }); // waypoint.name waypoint.getChild(namespace, "name").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { name = body; String content = body.trim(); // extremcaching.com manipulates the GC code by adding GC in front of ECxxx if (StringUtils.startsWithIgnoreCase(content, "GCEC") && StringUtils.containsIgnoreCase(scriptUrl, "extremcaching")) { content = content.substring(2); } cache.setName(content); findGeoCode(cache.getName()); } }); // waypoint.desc waypoint.getChild(namespace, "desc").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { desc = body; cache.setShortDescription(validate(body)); } }); // waypoint.cmt waypoint.getChild(namespace, "cmt").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { cmt = body; cache.setDescription(validate(body)); } }); // waypoint.getType() waypoint.getChild(namespace, "type").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { final String[] content = StringUtils.split(body, '|'); if (content.length > 0) { type = content[0].toLowerCase(Locale.US).trim(); } } }); // waypoint.sym waypoint.getChild(namespace, "sym").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { sym = body.toLowerCase(Locale.US); if (sym.contains("geocache") && sym.contains("found")) { cache.setFound(true); } } }); // waypoint.url waypoint.getChild(namespace, "url").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String url) { final MatcherWrapper matcher = new MatcherWrapper(PATTERN_GUID, url); if (matcher.matches()) { final String guid = matcher.group(1); if (StringUtils.isNotBlank(guid)) { cache.setGuid(guid); } } final MatcherWrapper matcherCode = new MatcherWrapper(PATTERN_URL_GEOCODE, url); if (matcherCode.matches()) { final String geocode = matcherCode.group(1); cache.setGeocode(geocode); } } }); // waypoint.urlname (name for waymarks) waypoint.getChild(namespace, "urlname").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String urlName) { if (cache.getName().equals(cache.getGeocode()) && StringUtils.startsWith(cache.getGeocode(), "WM")) { cache.setName(StringUtils.trim(urlName)); } } }); // for GPX 1.0, cache info comes from waypoint node (so called private children, // for GPX 1.1 from extensions node final Element cacheParent = getCacheParent(waypoint); registerGsakExtensions(cacheParent); registerTerraCachingExtensions(cacheParent); registerCgeoExtensions(cacheParent); // 3 different versions of the GC schema for (final String nsGC : GROUNDSPEAK_NAMESPACE) { // waypoints.cache final Element gcCache = cacheParent.getChild(nsGC, "cache"); gcCache.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { try { if (attrs.getIndex("id") > -1) { cache.setCacheId(attrs.getValue("id")); } if (attrs.getIndex("archived") > -1) { cache.setArchived(attrs.getValue("archived").equalsIgnoreCase("true")); } if (attrs.getIndex("available") > -1) { cache.setDisabled(!attrs.getValue("available").equalsIgnoreCase("true")); } } catch (final RuntimeException e) { Log.w("Failed to parse cache attributes", e); } } }); // waypoint.cache.getName() gcCache.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String cacheName) { cache.setName(validate(cacheName)); } }); // waypoint.cache.getOwner() gcCache.getChild(nsGC, "owner").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String ownerUserId) { cache.setOwnerUserId(validate(ownerUserId)); } }); // waypoint.cache.getOwner() gcCache.getChild(nsGC, "placed_by").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String ownerDisplayName) { cache.setOwnerDisplayName(validate(ownerDisplayName)); } }); // waypoint.cache.getType() gcCache.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String bodyIn) { String body = validate(bodyIn); // lab caches wrongly contain a prefix in the type if (body.startsWith("Geocache|")) { body = StringUtils.substringAfter(body, "Geocache|").trim(); } cache.setType(CacheType.getByPattern(body)); } }); // waypoint.cache.container gcCache.getChild(nsGC, "container").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { cache.setSize(CacheSize.getById(validate(body))); } }); // waypoint.cache.getAttributes() // @see issue #299 // <groundspeak:attributes> // <groundspeak:attribute id="32" inc="1">Bicycles</groundspeak:attribute> // <groundspeak:attribute id="13" inc="1">Available at all times</groundspeak:attribute> // where inc = 0 => _no, inc = 1 => _yes // IDs see array CACHE_ATTRIBUTES final Element gcAttributes = gcCache.getChild(nsGC, "attributes"); // waypoint.cache.attribute final Element gcAttribute = gcAttributes.getChild(nsGC, "attribute"); gcAttribute.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { try { if (attrs.getIndex("id") > -1 && attrs.getIndex("inc") > -1) { final int attributeId = Integer.parseInt(attrs.getValue("id")); final boolean attributeActive = Integer.parseInt(attrs.getValue("inc")) != 0; final CacheAttribute attribute = CacheAttribute.getById(attributeId); if (attribute != null) { cache.getAttributes().add(attribute.getValue(attributeActive)); } } } catch (final NumberFormatException ignored) { // nothing } } }); // waypoint.cache.getDifficulty() gcCache.getChild(nsGC, "difficulty").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { try { cache.setDifficulty(Float.parseFloat(body)); } catch (final NumberFormatException e) { Log.w("Failed to parse difficulty", e); } } }); // waypoint.cache.getTerrain() gcCache.getChild(nsGC, "terrain").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { try { cache.setTerrain(Float.parseFloat(body)); } catch (final NumberFormatException e) { Log.w("Failed to parse terrain", e); } } }); // waypoint.cache.country gcCache.getChild(nsGC, "country").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String country) { if (StringUtils.isBlank(cache.getLocation())) { cache.setLocation(validate(country)); } else { cache.setLocation(cache.getLocation() + ", " + country.trim()); } } }); // waypoint.cache.state gcCache.getChild(nsGC, "state").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String state) { final String trimmedState = state.trim(); if (StringUtils.isNotEmpty(trimmedState)) { // state can be completely empty if (StringUtils.isBlank(cache.getLocation())) { cache.setLocation(validate(state)); } else { cache.setLocation(trimmedState + ", " + cache.getLocation()); } } } }); // waypoint.cache.encoded_hints gcCache.getChild(nsGC, "encoded_hints").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String encoded) { cache.setHint(validate(encoded)); } }); gcCache.getChild(nsGC, "short_description").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String shortDesc) { cache.setShortDescription(validate(shortDesc)); } }); gcCache.getChild(nsGC, "long_description").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String desc) { cache.setDescription(validate(desc)); } }); // waypoint.cache.travelbugs final Element gcTBs = gcCache.getChild(nsGC, "travelbugs"); // waypoint.cache.travelbug final Element gcTB = gcTBs.getChild(nsGC, "travelbug"); // waypoint.cache.travelbugs.travelbug gcTB.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { trackable = new Trackable(); try { if (attrs.getIndex("ref") > -1) { trackable.setGeocode(attrs.getValue("ref")); } } catch (final RuntimeException ignored) { // nothing } } }); gcTB.setEndElementListener(new EndElementListener() { @Override public void end() { if (StringUtils.isNotBlank(trackable.getGeocode()) && StringUtils.isNotBlank(trackable.getName())) { cache.addInventoryItem(trackable); } } }); // waypoint.cache.travelbugs.travelbug.getName() gcTB.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String tbName) { trackable.setName(validate(tbName)); } }); // waypoint.cache.logs final Element gcLogs = gcCache.getChild(nsGC, "logs"); // waypoint.cache.log final Element gcLog = gcLogs.getChild(nsGC, "log"); gcLog.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { logBuilder = new LogEntry.Builder(); try { if (attrs.getIndex("id") > -1) { logBuilder.setId(Integer.parseInt(attrs.getValue("id"))); } } catch (final NumberFormatException ignored) { // nothing } } }); gcLog.setEndElementListener(new EndElementListener() { @Override public void end() { final LogEntry log = logBuilder.build(); if (log.getType() != LogType.UNKNOWN) { if (log.getType().isFoundLog() && StringUtils.isNotBlank(log.author)) { final IConnector connector = ConnectorFactory.getConnector(cache); if (connector instanceof ILogin && StringUtils.equals(log.author, ((ILogin) connector).getUserName())) { cache.setFound(true); cache.setVisitedDate(log.date); } } logs.add(log); } } }); // waypoint.cache.logs.log.date gcLog.getChild(nsGC, "date").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { try { logBuilder.setDate(parseDate(body).getTime()); } catch (final Exception e) { Log.w("Failed to parse log date", e); } } }); // waypoint.cache.logs.log.getType() gcLog.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { final String logType = validate(body); logBuilder.setLogType(LogType.getByType(logType)); } }); // waypoint.cache.logs.log.finder gcLog.getChild(nsGC, "finder").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String finderName) { logBuilder.setAuthor(validate(finderName)); } }); // waypoint.cache.logs.log.text gcLog.getChild(nsGC, "text").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String logText) { logBuilder.setLog(validate(logText)); } }); } try { progressStream = new ProgressInputStream(stream); final BufferedReader reader = new BufferedReader(new InputStreamReader(progressStream, CharEncoding.UTF_8)); Xml.parse(new InvalidXMLCharacterFilterReader(reader), root.getContentHandler()); return DataStore.loadCaches(result, EnumSet.of(LoadFlag.DB_MINIMAL)); } catch (final SAXException e) { throw new ParserException("Cannot parse .gpx file as GPX " + version + ": could not parse XML", e); } } /** * Add listeners for GSAK extensions * */ private void registerGsakExtensions(final Element cacheParent) { for (final String gsakNamespace : GSAK_NS) { final Element gsak = cacheParent.getChild(gsakNamespace, "wptExtension"); gsak.getChild(gsakNamespace, "Watch").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String watchList) { cache.setOnWatchlist(Boolean.parseBoolean(watchList.trim())); } }); gsak.getChild(gsakNamespace, "UserData").setEndTextElementListener(new UserDataListener(1)); for (int i = 2; i <= 4; i++) { gsak.getChild(gsakNamespace, "User" + i).setEndTextElementListener(new UserDataListener(i)); } gsak.getChild(gsakNamespace, "Parent").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { parentCacheCode = body; } }); gsak.getChild(gsakNamespace, "FavPoints").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String favoritePoints) { try { cache.setFavoritePoints(Integer.parseInt(favoritePoints)); } catch (final NumberFormatException e) { Log.w("Failed to parse favorite points", e); } } }); gsak.getChild(gsakNamespace, "GcNote").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String personalNote) { cache.setPersonalNote(StringUtils.trim(personalNote)); } }); gsak.getChild(gsakNamespace, "IsPremium").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String premium) { cache.setPremiumMembersOnly(Boolean.parseBoolean(premium)); } }); gsak.getChild(gsakNamespace, "LatBeforeCorrect").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String latitude) { originalLat = latitude; addOriginalCoordinates(); } }); gsak.getChild(gsakNamespace, "LonBeforeCorrect").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String longitude) { originalLon = longitude; addOriginalCoordinates(); } }); gsak.getChild(gsakNamespace, "Code").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String geocode) { if (StringUtils.isNotBlank(geocode)) { cache.setGeocode(StringUtils.trim(geocode)); } } }); } } /** * Add listeners for TerraCaching extensions * */ private void registerTerraCachingExtensions(final Element cacheParent) { final String terraNamespace = "http://www.TerraCaching.com/GPX/1/0"; final Element terraCache = cacheParent.getChild(terraNamespace, "terracache"); terraCache.getChild(terraNamespace, "name").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String name) { cache.setName(StringUtils.trim(name)); } }); terraCache.getChild(terraNamespace, "owner").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String ownerName) { cache.setOwnerDisplayName(validate(ownerName)); } }); terraCache.getChild(terraNamespace, "style").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String style) { cache.setType(TerraCachingType.getCacheType(style)); } }); terraCache.getChild(terraNamespace, "size").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String size) { cache.setSize(CacheSize.getById(size)); } }); terraCache.getChild(terraNamespace, "country").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String country) { if (StringUtils.isNotBlank(country)) { cache.setLocation(StringUtils.trim(country)); } } }); terraCache.getChild(terraNamespace, "state").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String state) { final String trimmedState = state.trim(); if (StringUtils.isNotEmpty(trimmedState)) { if (StringUtils.isBlank(cache.getLocation())) { cache.setLocation(validate(state)); } else { cache.setLocation(trimmedState + ", " + cache.getLocation()); } } } }); terraCache.getChild(terraNamespace, "description").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String description) { cache.setDescription(trimHtml(description)); } }); terraCache.getChild(terraNamespace, "hint").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String hint) { cache.setHint(HtmlUtils.extractText(hint)); } }); final Element terraLogs = terraCache.getChild(terraNamespace, "logs"); final Element terraLog = terraLogs.getChild(terraNamespace, "log"); terraLog.setStartElementListener(new StartElementListener() { @Override public void start(final Attributes attrs) { logBuilder = new LogEntry.Builder(); try { if (attrs.getIndex("id") > -1) { logBuilder.setId(Integer.parseInt(attrs.getValue("id"))); } } catch (final NumberFormatException ignored) { // nothing } } }); terraLog.setEndElementListener(new EndElementListener() { @Override public void end() { final LogEntry log = logBuilder.build(); if (log.getType() != LogType.UNKNOWN) { if (log.getType().isFoundLog() && StringUtils.isNotBlank(log.author)) { final IConnector connector = ConnectorFactory.getConnector(cache); if (connector instanceof ILogin && StringUtils.equals(log.author, ((ILogin) connector).getUserName())) { cache.setFound(true); cache.setVisitedDate(log.date); } } logs.add(log); } } }); // waypoint.cache.logs.log.date terraLog.getChild(terraNamespace, "date").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { try { logBuilder.setDate(parseDate(body).getTime()); } catch (final Exception e) { Log.w("Failed to parse log date", e); } } }); // waypoint.cache.logs.log.type terraLog.getChild(terraNamespace, "type").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String body) { final String logType = validate(body); logBuilder.setLogType(TerraCachingLogType.getLogType(logType)); } }); // waypoint.cache.logs.log.finder terraLog.getChild(terraNamespace, "user").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String finderName) { logBuilder.setAuthor(validate(finderName)); } }); // waypoint.cache.logs.log.text terraLog.getChild(terraNamespace, "entry").setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String entry) { logBuilder.setLog(trimHtml(validate(entry))); } }); } private static String trimHtml(final String html) { return StringUtils.trim(StringUtils.removeEnd(StringUtils.removeStart(html, "<br>"), "<br>")); } protected void addOriginalCoordinates() { if (StringUtils.isNotEmpty(originalLat) && StringUtils.isNotEmpty(originalLon)) { final Waypoint waypoint = new Waypoint(CgeoApplication.getInstance().getString(R.string.cache_coordinates_original), WaypointType.ORIGINAL, false); waypoint.setCoords(new Geopoint(originalLat, originalLon)); cache.addOrChangeWaypoint(waypoint, false); cache.setUserModifiedCoords(true); } } /** * Add listeners for c:geo extensions * */ private void registerCgeoExtensions(final Element cacheParent) { final Element cgeoVisited = cacheParent.getChild(CGEO_NS, "visited"); cgeoVisited.setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String visited) { wptVisited = Boolean.parseBoolean(visited.trim()); } }); final Element cgeoUserDefined = cacheParent.getChild(CGEO_NS, "userdefined"); cgeoUserDefined.setEndTextElementListener(new EndTextElementListener() { @Override public void end(final String userDefined) { wptUserDefined = Boolean.parseBoolean(userDefined.trim()); } }); } /** * Overwrite this method in a GPX parser sub class to modify the {@link Geocache}, after it has been fully parsed * from the GPX file and before it gets stored. * * @param cache * currently imported cache */ protected void afterParsing(final Geocache cache) { if ("GC_WayPoint1".equals(cache.getShortDescription())) { cache.setShortDescription(""); } } /** * GPX 1.0 and 1.1 use different XML elements to put the cache into, therefore needs to be overwritten in the * version specific subclasses * */ protected abstract Element getCacheParent(Element waypoint); protected static String validate(final String input) { if ("nil".equalsIgnoreCase(input)) { return ""; } return input.trim(); } private void findGeoCode(final String input) { if (input == null || StringUtils.isNotBlank(cache.getGeocode())) { return; } final String trimmed = input.trim(); final MatcherWrapper matcherGeocode = new MatcherWrapper(PATTERN_GEOCODE, trimmed); if (matcherGeocode.find()) { final String geocode = matcherGeocode.group(); // a geocode should not be part of a word if (geocode.length() == trimmed.length() || Character.isWhitespace(trimmed.charAt(geocode.length()))) { if (ConnectorFactory.canHandle(geocode)) { cache.setGeocode(geocode); } } } } /** * reset all fields that are used to store cache fields over the duration of parsing a single cache */ private void resetCache() { type = null; sym = null; name = null; desc = null; cmt = null; parentCacheCode = null; wptVisited = false; wptUserDefined = false; logs = new ArrayList<>(); cache = createCache(); // explicitly set all properties which could lead to database access, if left as null value cache.setLocation(""); cache.setDescription(""); cache.setShortDescription(""); cache.setHint(""); for (int i = 0; i < userData.length; i++) { userData[i] = null; } originalLon = null; originalLat = null; } /** * Geocache factory method. This explicitly sets several members to empty lists, which does not happen with the * default constructor. */ private static Geocache createCache() { final Geocache newCache = new Geocache(); newCache.setReliableLatLon(true); // always assume correct coordinates, when importing from file instead of website newCache.setAttributes(Collections.<String> emptyList()); // override the lazy initialized list newCache.setWaypoints(Collections.<Waypoint> emptyList(), false); // override the lazy initialized list return newCache; } /** * create a cache note from the UserData1 to UserData4 fields supported by GSAK */ private void createNoteFromGSAKUserdata() { if (StringUtils.isBlank(cache.getPersonalNote())) { final StringBuilder buffer = new StringBuilder(); for (final String anUserData : userData) { if (StringUtils.isNotBlank(anUserData)) { buffer.append(' ').append(anUserData); } } final String note = buffer.toString().trim(); if (StringUtils.isNotBlank(note)) { cache.setPersonalNote(note); } } } private boolean isValidForImport() { if (StringUtils.isBlank(cache.getGeocode())) { return false; } if (cache.getCoords() == null) { return false; } final boolean valid = (type == null && sym == null) || StringUtils.contains(type, "geocache") || StringUtils.contains(sym, "geocache") || StringUtils.containsIgnoreCase(sym, "waymark") || (StringUtils.containsIgnoreCase(sym, "terracache") && !terraChildWaypoint); if ("GC_WayPoint1".equals(cache.getShortDescription())) { terraChildWaypoint = true; } return valid; } @Nullable private Geocache findParentCache() { if (StringUtils.isBlank(parentCacheCode)) { return null; } // first match by geocode only Geocache cacheForWaypoint = DataStore.loadCache(parentCacheCode, LoadFlags.LOAD_CACHE_OR_DB); if (cacheForWaypoint == null) { // then match by title final String geocode = DataStore.getGeocodeForTitle(parentCacheCode); if (StringUtils.isNotBlank(geocode)) { cacheForWaypoint = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); } } return cacheForWaypoint; } }