package cgeo.geocaching.gcvote; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.connector.capability.ICredentials; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Credentials; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Charsets; import cgeo.geocaching.utils.LeastRecentlyUsedMap; import cgeo.geocaching.utils.Log; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import android.support.annotation.StringRes; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public final class GCVote implements ICredentials { // gcvote.com does not have a https certificate. However, Guido (the owner of gcvote.com) told // us on 2017-03-21 that the site is accessible through its provider https endpoint at // https://ssl.webpack.de/gcvote.com public static final float NO_RATING = 0; private static final int MAX_CACHED_RATINGS = 1000; private static final LeastRecentlyUsedMap<String, GCVoteRating> RATINGS_CACHE = new LeastRecentlyUsedMap.LruCache<>(MAX_CACHED_RATINGS); private static final float MIN_RATING = 1; private static final float MAX_RATING = 5; private GCVote() { // utility class } private static class SingletonHolder { @NonNull private static final GCVote INSTANCE = new GCVote(); } @NonNull public static GCVote getInstance() { return SingletonHolder.INSTANCE; } /** * Get user rating for a given guid or geocode. For a guid first the ratings cache is checked * before a request to gcvote.com is made. */ @Nullable public static GCVoteRating getRating(final String guid, final String geocode) { if (StringUtils.isNotBlank(guid) && RATINGS_CACHE.containsKey(guid)) { return RATINGS_CACHE.get(guid); } final Map<String, GCVoteRating> ratings = getRating(singletonOrNull(guid), singletonOrNull(geocode)); return MapUtils.isNotEmpty(ratings) ? ratings.values().iterator().next() : null; } @Nullable private static List<String> singletonOrNull(final String item) { return StringUtils.isNotBlank(item) ? Collections.singletonList(item) : null; } /** * Get user ratings from gcvote.com */ @NonNull private static Map<String, GCVoteRating> getRating(final List<String> guids, final List<String> geocodes) { if (guids == null && geocodes == null) { return Collections.emptyMap(); } final Parameters params = new Parameters("version", "cgeo"); final Credentials login = Settings.getGCVoteLogin(); if (login.isValid()) { params.put("userName", login.getUserName(), "password", login.getPassword()); } // use guid or gccode for lookup final boolean requestByGuids = CollectionUtils.isNotEmpty(guids); if (requestByGuids) { params.put("cacheIds", StringUtils.join(guids, ',')); } else { params.put("waypoints", StringUtils.join(geocodes, ',')); } final InputStream response = Network.getResponseStream(Network.getRequest("https://ssl.webpack.de/gcvote.com/getVotes.php", params)); if (response == null) { return Collections.emptyMap(); } try { return getRatingsFromXMLResponse(response, requestByGuids); } finally { IOUtils.closeQuietly(response); } } @NonNull static Map<String, GCVoteRating> getRatingsFromXMLResponse(@NonNull final InputStream response, final boolean requestByGuids) { try { final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); final XmlPullParser xpp = factory.newPullParser(); xpp.setInput(response, Charsets.UTF_8.name()); boolean loggedIn = false; final Map<String, GCVoteRating> ratings = new HashMap<>(); int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { final String tagName = xpp.getName(); if (StringUtils.equals(tagName, "vote")) { final String id = xpp.getAttributeValue(null, requestByGuids ? "cacheId" : "waypoint"); final float myVote = loggedIn ? Float.parseFloat(xpp.getAttributeValue(null, "voteUser")) : 0; final GCVoteRating voteRating = new GCVoteRating(Float.parseFloat(xpp.getAttributeValue(null, "voteAvg")), Integer.parseInt(xpp.getAttributeValue(null, "voteCnt")), myVote); ratings.put(id, voteRating); } else if (StringUtils.equals(tagName, "votes")) { loggedIn = StringUtils.equals(xpp.getAttributeValue(null, "loggedIn"), "true"); } } eventType = xpp.next(); } RATINGS_CACHE.putAll(ratings); return ratings; } catch (final NumberFormatException | XmlPullParserException | IOException e) { Log.e("Cannot parse GCVote result", e); return Collections.emptyMap(); } } /** * Transmit user vote to gcvote.com * * @param cache the geocache (supported by GCVote) * @param rating the rating * @return {@code true} if the rating was submitted successfully */ public static boolean setRating(@NonNull final Geocache cache, final float rating) { if (!isVotingPossible(cache)) { throw new IllegalArgumentException("voting is not possible for " + cache); } if (!isValidRating(rating)) { throw new IllegalArgumentException("invalid rating " + rating); } final Credentials login = Settings.getGCVoteLogin(); if (login.isInvalid()) { Log.e("GCVote.setRating: cannot find credentials"); return false; } final Parameters params = new Parameters( "userName", login.getUserName(), "password", login.getPassword(), "cacheId", cache.getGuid(), "waypoint", cache.getGeocode(), "voteUser", String.format(Locale.US, "%.1f", rating), "version", "cgeo"); final String result = StringUtils.trim(Network.getResponseData(Network.getRequest("https://ssl.webpack.de/gcvote.com/setVote.php", params))); if (!StringUtils.equalsIgnoreCase(result, "ok")) { Log.e("GCVote.setRating: could not post rating, answer was " + result); return false; } return true; } public static void loadRatings(@NonNull final List<Geocache> caches) { if (!Settings.isRatingWanted()) { return; } final List<String> geocodes = getVotableGeocodes(caches); if (geocodes.isEmpty()) { return; } try { final Map<String, GCVoteRating> ratings = getRating(null, geocodes); // save found cache coordinates for (final Geocache cache : caches) { if (ratings.containsKey(cache.getGeocode())) { final GCVoteRating rating = ratings.get(cache.getGeocode()); cache.setRating(rating.getRating()); cache.setVotes(rating.getVotes()); cache.setMyVote(rating.getMyVote()); } } } catch (final Exception e) { Log.e("GCVote.loadRatings", e); } } /** * Get geocodes of all the caches, which can be used with GCVote. Non-GC caches will be filtered out. */ @NonNull private static List<String> getVotableGeocodes(@NonNull final Collection<Geocache> caches) { final List<String> geocodes = new ArrayList<>(caches.size()); for (final Geocache cache : caches) { final String geocode = cache.getGeocode(); if (StringUtils.isNotBlank(geocode) && cache.supportsGCVote()) { geocodes.add(geocode); } } return geocodes; } public static boolean isValidRating(final float rating) { return rating >= MIN_RATING && rating <= MAX_RATING; } public static boolean isVotingPossible(@NonNull final Geocache cache) { return Settings.isGCVoteLoginValid() && StringUtils.isNotBlank(cache.getGuid()) && cache.supportsGCVote(); } static String getDescription(final float rating) { switch (Math.round(rating * 2f)) { case 2: return getString(R.string.log_stars_1_description); case 3: return getString(R.string.log_stars_15_description); case 4: return getString(R.string.log_stars_2_description); case 5: return getString(R.string.log_stars_25_description); case 6: return getString(R.string.log_stars_3_description); case 7: return getString(R.string.log_stars_35_description); case 8: return getString(R.string.log_stars_4_description); case 9: return getString(R.string.log_stars_45_description); case 10: return getString(R.string.log_stars_5_description); default: return getString(R.string.log_no_rating); } } private static String getString(@StringRes final int resId) { return CgeoApplication.getInstance().getString(resId); } @NonNull public static String getWebsite() { return "http://gcvote.com"; } @NonNull public static String getCreateAccountUrl() { return "http://gcvote.com/help_en.php"; } @Override public int getUsernamePreferenceKey() { return R.string.pref_user_vote; } @Override public int getPasswordPreferenceKey() { return R.string.pref_pass_vote; } @Override public int getAvatarPreferenceKey() { return R.string.pref_gcvote_avatar; } @Override public Credentials getCredentials() { return Settings.getCredentials(R.string.pref_user_vote, R.string.pref_pass_vote); } }