package cgeo.geocaching.connector.oc;
import cgeo.geocaching.Geocache;
import cgeo.geocaching.Image;
import cgeo.geocaching.LogEntry;
import cgeo.geocaching.R;
import cgeo.geocaching.Settings;
import cgeo.geocaching.cgeoapplication;
import cgeo.geocaching.connector.ConnectorFactory;
import cgeo.geocaching.connector.IConnector;
import cgeo.geocaching.connector.gc.GCConnector;
import cgeo.geocaching.enumerations.CacheAttribute;
import cgeo.geocaching.enumerations.CacheSize;
import cgeo.geocaching.enumerations.CacheType;
import cgeo.geocaching.enumerations.LogType;
import cgeo.geocaching.geopoint.Geopoint;
import cgeo.geocaching.utils.Log;
import org.apache.commons.lang3.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import android.content.res.Resources;
import android.sax.Element;
import android.sax.EndElementListener;
import android.sax.EndTextElementListener;
import android.sax.RootElement;
import android.sax.StartElementListener;
import android.util.Xml;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OC11XMLParser {
private static final String[] MARKUP = new String[] { "p", "span" };
private static Pattern STRIP_DATE = Pattern.compile("\\+0([0-9]){1}\\:00");
private static Pattern LOCAL_URL = Pattern.compile("href=\"(.*)\"");
private static final int CACHE_PARSE_LIMIT = 250;
private static final Resources res = cgeoapplication.getInstance().getResources();
private static final Pattern WHITESPACE = Pattern.compile("
(\\s| )*
");
private static ImageHolder imageHolder = null;
private static class CacheHolder {
public Geocache cache;
public String latitude;
public String longitude;
}
private static class CacheLog {
public String id;
public String cacheId;
public LogEntry logEntry;
}
private static class CacheDescription {
public String cacheId;
public String shortDesc;
public String desc;
public String hint;
}
private static class ImageHolder {
public String url;
public String objectId;
protected String title;
protected boolean isSpoiler = false;
}
private static Date parseFullDate(final String date) {
final SimpleDateFormat ISO8601DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
ISO8601DATEFORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
final String strippedDate = STRIP_DATE.matcher(date).replaceAll("+0$100");
try {
return ISO8601DATEFORMAT.parse(strippedDate);
} catch (ParseException e) {
Log.e("OC11XMLParser.parseFullDate", e);
}
return null;
}
private static Date parseDayDate(final String date) {
final SimpleDateFormat ISO8601DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
ISO8601DATEFORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
final String strippedDate = STRIP_DATE.matcher(date).replaceAll("+0$100");
try {
return ISO8601DATEFORMAT.parse(strippedDate);
} catch (ParseException e) {
Log.e("OC11XMLParser.parseDayDate", e);
}
return null;
}
private static CacheSize getCacheSize(final String sizeId) {
try {
int size = Integer.parseInt(sizeId);
switch (size) {
case 1:
return CacheSize.OTHER;
case 2:
return CacheSize.MICRO;
case 3:
return CacheSize.SMALL;
case 4:
return CacheSize.REGULAR;
case 5:
case 6:
return CacheSize.LARGE;
case 8:
return CacheSize.VIRTUAL;
default:
break;
}
} catch (NumberFormatException e) {
Log.e("OC11XMLParser.getCacheSize", e);
}
return CacheSize.NOT_CHOSEN;
}
private static CacheType getCacheType(final String typeId) {
try {
int type = Integer.parseInt(typeId);
switch (type) {
case 1: // Other/unbekannter Cachetyp
return CacheType.UNKNOWN;
case 2: // Trad./normaler Cache
return CacheType.TRADITIONAL;
case 3: // Multi/Multicache
return CacheType.MULTI;
case 4: // Virt./virtueller Cache
return CacheType.VIRTUAL;
case 5: // ICam./Webcam-Cache
return CacheType.WEBCAM;
case 6: // Event/Event-Cache
return CacheType.EVENT;
case 7: // Quiz/Rätselcache
return CacheType.MYSTERY;
case 8: // Math/Mathe-/Physikcache
return CacheType.MYSTERY;
case 9: // Moving/beweglicher Cache
return CacheType.VIRTUAL;
case 10: // Driv./Drive-In
return CacheType.TRADITIONAL;
default:
return CacheType.UNKNOWN;
}
} catch (NumberFormatException e) {
Log.e("OC11XMLParser.getCacheType", e);
}
return CacheType.UNKNOWN;
}
private static LogType getLogType(final int typeId) {
switch (typeId) {
case 1:
return LogType.FOUND_IT;
case 2:
return LogType.DIDNT_FIND_IT;
case 3:
return LogType.NOTE;
case 7:
return LogType.ATTENDED;
case 8:
return LogType.WILL_ATTEND;
default:
return LogType.UNKNOWN;
}
}
private static void setCacheStatus(final int statusId, final Geocache cache) {
switch (statusId) {
case 1:
cache.setArchived(false);
cache.setDisabled(false);
break;
case 2:
cache.setArchived(false);
cache.setDisabled(true);
break;
default:
cache.setArchived(true);
cache.setDisabled(false);
break;
}
}
private static void resetCache(final CacheHolder cacheHolder) {
cacheHolder.cache = new Geocache(null);
cacheHolder.cache.setReliableLatLon(true);
cacheHolder.cache.setDescription(StringUtils.EMPTY);
cacheHolder.latitude = "0.0";
cacheHolder.longitude = "0.0";
}
private static void resetLog(final CacheLog log) {
log.cacheId = StringUtils.EMPTY;
log.logEntry = new LogEntry("", 0, LogType.UNKNOWN, "");
}
private static void resetDesc(final CacheDescription desc) {
desc.cacheId = StringUtils.EMPTY;
desc.shortDesc = StringUtils.EMPTY;
desc.desc = StringUtils.EMPTY;
desc.hint = StringUtils.EMPTY;
}
private static int attributeId;
public static Collection parseCaches(final InputStream stream) throws IOException {
// parse and return caches without filtering
return parseCaches(stream, true);
}
public static Collection parseCachesFiltered(final InputStream stream) throws IOException {
// parse caches and filter result
return parseCaches(stream, false);
}
private static Collection parseCaches(final InputStream stream, boolean ignoreFiltersIn) throws IOException {
final Map caches = new HashMap();
final Map logs = new HashMap();
final CacheHolder cacheHolder = new CacheHolder();
final CacheLog logHolder = new CacheLog();
final CacheDescription descHolder = new CacheDescription();
final RootElement root = new RootElement("oc11xml");
final Element cacheNode = root.getChild("cache");
final boolean ignoreFilters = ignoreFiltersIn;
// cache
cacheNode.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attributes) {
resetCache(cacheHolder);
}
});
cacheNode.setEndElementListener(new EndElementListener() {
@Override
public void end() {
Geocache cache = cacheHolder.cache;
Geopoint coords = new Geopoint(cacheHolder.latitude, cacheHolder.longitude);
cache.setCoords(coords);
if (caches.size() < CACHE_PARSE_LIMIT && isValid(cache) && (ignoreFilters || !isExcluded(cache))) {
cache.setDetailedUpdatedNow();
caches.put(cache.getCacheId(), cache);
}
}
private boolean isExcluded(Geocache cache) {
if (cache.isArchived()) {
return true;
}
if (cache.isDisabled() && Settings.isExcludeDisabledCaches()) {
return true;
}
if ((cache.isFound() || cache.isOwner()) && Settings.isExcludeMyCaches()) {
return true;
}
return !Settings.getCacheType().contains(cache);
}
private boolean isValid(Geocache cache) {
return StringUtils.isNotBlank(cache.getGeocode()) && !cache.getCoords().equals(Geopoint.ZERO);
}
});
// cache.id
cacheNode.getChild("id").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
cacheHolder.cache.setCacheId(body);
}
});
// cache.longitude
cacheNode.getChild("longitude").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
String longitude = body.trim();
if (StringUtils.isNotBlank(longitude)) {
cacheHolder.longitude = longitude;
}
}
});
// cache.latitude
cacheNode.getChild("latitude").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
String latitude = body.trim();
if (StringUtils.isNotBlank(latitude)) {
cacheHolder.latitude = latitude;
}
}
});
// cache.name
cacheNode.getChild("name").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
cacheHolder.cache.setName(content);
}
});
// cache.waypoints[oc]
cacheNode.getChild("waypoints").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
if (attrs.getIndex("oc") > -1) {
cacheHolder.cache.setGeocode(attrs.getValue("oc"));
}
if (attrs.getIndex("gccom") > -1) {
String gccode = attrs.getValue("gccom");
if (!StringUtils.isBlank(gccode)) {
cacheHolder.cache.setDescription(res.getString(R.string.cache_listed_on, GCConnector.getInstance().getName()) + ": " + gccode + "
");
}
}
}
});
// cache.type[id]
cacheNode.getChild("type").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
if (attrs.getIndex("id") > -1) {
final String typeId = attrs.getValue("id");
cacheHolder.cache.setType(getCacheType(typeId));
}
}
});
// cache.status[id]
cacheNode.getChild("status").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
if (attrs.getIndex("id") > -1) {
try {
final int statusId = Integer.parseInt(attrs.getValue("id"));
setCacheStatus(statusId, cacheHolder.cache);
} catch (NumberFormatException e) {
Log.w(String.format("Failed to parse status of cache '%s'.", cacheHolder.cache.getGeocode()));
}
}
}
});
// cache.size[id]
cacheNode.getChild("size").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
if (attrs.getIndex("id") > -1) {
final String typeId = attrs.getValue("id");
cacheHolder.cache.setSize(getCacheSize(typeId));
}
}
});
// cache.difficulty
cacheNode.getChild("difficulty").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
try {
cacheHolder.cache.setDifficulty(Float.valueOf(content));
} catch (NumberFormatException e) {
Log.e("OC11XMLParser: unknown difficulty " + content, e);
}
}
});
// cache.terrain
cacheNode.getChild("terrain").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
try {
cacheHolder.cache.setTerrain(Float.valueOf(content));
} catch (NumberFormatException e) {
Log.e("OC11XMLParser: unknown terrain " + content, e);
}
}
});
// cache.datehidden
cacheNode.getChild("datehidden").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
cacheHolder.cache.setHidden(parseFullDate(content));
}
});
// cache.userid
final Element useridNode = cacheNode.getChild("userid");
useridNode.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attributes) {
if (attributes.getIndex("id") > -1) {
cacheHolder.cache.setOwnerUserId(attributes.getValue("id"));
}
}
});
useridNode.setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
cacheHolder.cache.setOwnerDisplayName(body);
}
});
// cache.attributes.attribute
final Element attributeNode = cacheNode.getChild("attributes").getChild("attribute");
attributeNode.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attributes) {
if (attributes.getIndex("id") > -1) {
try {
attributeId = Integer.parseInt(attributes.getValue("id"));
} catch (NumberFormatException e) {
Log.w(String.format("Failed to parse attribute id of cache '%s'.", cacheHolder.cache.getGeocode()));
}
}
}
});
attributeNode.setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
CacheAttribute attribute = CacheAttribute.getByOcId(attributeId);
if (attribute != null) {
// semantic of attributes on opencaching is always "yes"
cacheHolder.cache.getAttributes().add(attribute.getAttributeName(true));
}
else {
if (StringUtils.isNotBlank(body)) {
cacheHolder.cache.getAttributes().add(body.trim());
}
}
}
});
// cachedesc
final Element cacheDesc = root.getChild("cachedesc");
cacheDesc.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attributes) {
resetDesc(descHolder);
}
});
cacheDesc.setEndElementListener(new EndElementListener() {
@Override
public void end() {
final Geocache cache = caches.get(descHolder.cacheId);
if (cache != null) {
cache.setShortDescription(descHolder.shortDesc);
cache.setDescription(cache.getDescription() + descHolder.desc);
cache.setHint(descHolder.hint);
}
}
});
// cachedesc.cacheid
cacheDesc.getChild("cacheid").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
descHolder.cacheId = body;
}
});
// cachedesc.desc
cacheDesc.getChild("shortdesc").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
descHolder.shortDesc = linkify(stripEmptyText(content));
}
});
// cachedesc.desc
cacheDesc.getChild("desc").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
final String content = body.trim();
descHolder.desc = linkify(stripEmptyText(content));
}
});
// cachedesc.hint
cacheDesc.getChild("hint").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
descHolder.hint = body.trim();
}
});
// cachelog
final Element cacheLog = root.getChild("cachelog");
cacheLog.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
resetLog(logHolder);
}
});
cacheLog.setEndElementListener(new EndElementListener() {
@Override
public void end() {
final Geocache cache = caches.get(logHolder.cacheId);
if (cache != null && logHolder.logEntry.type != LogType.UNKNOWN) {
logs.put(logHolder.id, logHolder.logEntry);
cache.getLogs().add(0, logHolder.logEntry);
if ((logHolder.logEntry.type == LogType.FOUND_IT || logHolder.logEntry.type == LogType.ATTENDED)
&& StringUtils.equalsIgnoreCase(logHolder.logEntry.author, Settings.getOCConnectorUserName())) {
cache.setFound(true);
cache.setVisitedDate(logHolder.logEntry.date);
}
}
}
});
// cachelog.id
cacheLog.getChild("id").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
logHolder.id = StringUtils.trim(body);
}
});
// cachelog.cacheid
cacheLog.getChild("cacheid").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
logHolder.cacheId = body;
}
});
// cachelog.date
cacheLog.getChild("date").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
try {
logHolder.logEntry.date = parseDayDate(body).getTime();
} catch (NullPointerException e) {
Log.w("Failed to parse log date", e);
}
}
});
// cachelog.logtype
cacheLog.getChild("logtype").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
if (attrs.getIndex("id") > -1) {
final String id = attrs.getValue("id");
try {
final int typeId = Integer.parseInt(id);
logHolder.logEntry.type = getLogType(typeId);
} catch (NumberFormatException e) {
Log.e("OC11XMLParser, unknown logtype " + id, e);
}
}
}
});
// cachelog.userid
cacheLog.getChild("userid").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String finderName) {
logHolder.logEntry.author = finderName;
}
});
// cachelog.text
cacheLog.getChild("text").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String logText) {
logHolder.logEntry.log = stripEmptyText(logText);
}
});
// pictures
final Element picture = root.getChild("picture");
picture.setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attrs) {
imageHolder = new ImageHolder();
}
});
picture.setEndElementListener(new EndElementListener() {
@Override
public void end() {
if (imageHolder.isSpoiler) {
final Geocache cache = caches.get(imageHolder.objectId);
if (cache != null) {
Image spoiler = new Image(imageHolder.url, imageHolder.title);
cache.addSpoiler(spoiler);
}
}
else {
final LogEntry log = logs.get(imageHolder.objectId);
if (log != null) {
log.addLogImage(new Image(imageHolder.url, imageHolder.title));
}
}
}
});
// picture.object
picture.getChild("object").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
imageHolder.objectId = StringUtils.trim(body);
}
});
// picture.title
picture.getChild("title").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
imageHolder.title = StringUtils.trim(body);
}
});
// picture.url
picture.getChild("url").setEndTextElementListener(new EndTextElementListener() {
@Override
public void end(String body) {
imageHolder.url = StringUtils.trim(body);
}
});
// picture.attributes
picture.getChild("attributes").setStartElementListener(new StartElementListener() {
@Override
public void start(Attributes attributes) {
if (attributes.getIndex("spoiler") > -1) {
String spoiler = attributes.getValue("spoiler");
imageHolder.isSpoiler = ("1".equals(spoiler));
}
}
});
try {
Xml.parse(stream, Xml.Encoding.UTF_8, root.getContentHandler());
return caches.values();
} catch (SAXException e) {
Log.e("Cannot parse .gpx file as oc11xml: could not parse XML", e);
return null;
}
}
/**
* Converts local links to absolute links targeting the OC website.
*/
private static String linkify(String input) {
String result = input;
Matcher matcher = LOCAL_URL.matcher(result);
while (matcher.find()) {
String url = matcher.group(1);
if (!url.contains(":/")) {
IConnector ocConnector = ConnectorFactory.getConnector("OCXXX");
String prefix = "http://" + ocConnector.getHost() + "/";
result = StringUtils.replace(result, url, prefix + url);
matcher = LOCAL_URL.matcher(result);
}
}
return result;
}
/**
* Removes some unneeded markup and whitespace. Log texts are typically encapsulated in paragraph tags which lead to
* more empty space on rendering.
*/
protected static String stripEmptyText(String input) {
final Matcher matcher = WHITESPACE.matcher(input);
String result = matcher.replaceAll("").trim();
if (!StringUtils.startsWith(result, "<")) {
return result;
}
return stripMarkup(result);
}
private static String stripMarkup(final String input) {
String result = input;
for (String tagName : MARKUP) {
final String startTag = "<" + tagName + ">";
if (StringUtils.startsWith(result, startTag)) {
final String endTag = "" + tagName + ">";
if (StringUtils.endsWith(result, endTag)) {
String inner = result.substring(startTag.length(), result.length() - endTag.length()).trim();
String nested = stripMarkup(inner);
if (!nested.contains(startTag)) {
result = nested;
}
}
}
}
return result;
}
}