package cgeo.geocaching; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.LoadFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.gcvote.GCVote; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.Log; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.functions.BiFunction; import io.reactivex.functions.Function; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; public class SearchResult implements Parcelable { private final Set<String> geocodes; private final Set<String> filteredGeocodes; @NonNull private StatusCode error = StatusCode.NO_ERROR; private String url = ""; private String[] viewstates = null; /** * Overall number of search results matching our search on geocaching.com. If this number is higher than 20, we have * to fetch multiple pages to get all caches. */ private int totalCountGC = 0; public static final Parcelable.Creator<SearchResult> CREATOR = new Parcelable.Creator<SearchResult>() { @Override public SearchResult createFromParcel(final Parcel in) { return new SearchResult(in); } @Override public SearchResult[] newArray(final int size) { return new SearchResult[size]; } }; /** * Build a new empty search result. */ public SearchResult() { this(new HashSet<String>()); } /** * Build a new empty search result with an error status. */ public SearchResult(@NonNull final StatusCode statusCode) { this(); error = statusCode; } /** * Copy a search result, for example to apply different filters on it. * * @param searchResult the original search result, which cannot be null */ public SearchResult(final SearchResult searchResult) { geocodes = new HashSet<>(searchResult.geocodes); filteredGeocodes = new HashSet<>(searchResult.filteredGeocodes); error = searchResult.error; url = searchResult.url; viewstates = searchResult.viewstates; setTotalCountGC(searchResult.getTotalCountGC()); } /** * Build a search result from an existing collection of geocodes. * * @param geocodes * a non-null collection of geocodes * @param totalCountGC * the total number of caches matching that search on geocaching.com (as we always get only the next 20 * from a web page) */ public SearchResult(final Collection<String> geocodes, final int totalCountGC) { this.geocodes = new HashSet<>(geocodes.size()); this.geocodes.addAll(geocodes); this.filteredGeocodes = new HashSet<>(); this.setTotalCountGC(totalCountGC); } /** * Build a search result from an existing collection of geocodes. * * @param geocodes a non-null set of geocodes */ public SearchResult(final Set<String> geocodes) { this(geocodes, geocodes.size()); } public SearchResult(final Parcel in) { final ArrayList<String> list = new ArrayList<>(); in.readStringList(list); geocodes = new HashSet<>(list); final ArrayList<String> filteredList = new ArrayList<>(); in.readStringList(filteredList); filteredGeocodes = new HashSet<>(filteredList); error = (StatusCode) in.readSerializable(); url = in.readString(); final int length = in.readInt(); if (length >= 0) { viewstates = new String[length]; in.readStringArray(viewstates); } setTotalCountGC(in.readInt()); } /** * Build a search result designating a single cache. * * @param cache the cache to include */ public SearchResult(final Geocache cache) { this(Collections.singletonList(cache)); } /** * Build a search result from a collection of caches. * * @param caches the non-null collection of caches to include */ public SearchResult(@NonNull final Collection<Geocache> caches) { this(); addAndPutInCache(caches); } @Override public void writeToParcel(final Parcel out, final int flags) { out.writeStringArray(geocodes.toArray(new String[geocodes.size()])); out.writeStringArray(filteredGeocodes.toArray(new String[filteredGeocodes.size()])); out.writeSerializable(error); out.writeString(url); if (viewstates == null) { out.writeInt(-1); } else { out.writeInt(viewstates.length); out.writeStringArray(viewstates); } out.writeInt(getTotalCountGC()); } @Override public int describeContents() { return 0; } @NonNull public Set<String> getGeocodes() { return Collections.unmodifiableSet(geocodes); } public int getCount() { return geocodes.size(); } @NonNull public StatusCode getError() { return error; } public void setError(@NonNull final StatusCode error) { this.error = error; } public String getUrl() { return url; } public void setUrl(final String url) { this.url = url; } public String[] getViewstates() { return viewstates; } public void setViewstates(final String[] viewstates) { if (GCLogin.isEmpty(viewstates)) { return; } // lazy initialization of viewstates if (this.viewstates == null) { this.viewstates = new String[viewstates.length]; } System.arraycopy(viewstates, 0, this.viewstates, 0, viewstates.length); } public int getTotalCountGC() { return totalCountGC; } public void setTotalCountGC(final int totalCountGC) { this.totalCountGC = totalCountGC; } public SearchResult filterSearchResults(final boolean excludeDisabled, final CacheType cacheType) { final SearchResult result = new SearchResult(this); result.geocodes.clear(); final List<Geocache> includedCaches = new ArrayList<>(); final Set<Geocache> caches = DataStore.loadCaches(geocodes, LoadFlags.LOAD_CACHE_OR_DB); int excluded = 0; for (final Geocache cache : caches) { // Is there any reason to exclude the cache from the list? final boolean excludeCache = (excludeDisabled && (cache.isDisabled() || cache.isArchived())) || !cacheType.contains(cache); if (excludeCache) { excluded++; } else { includedCaches.add(cache); } } result.addAndPutInCache(includedCaches); // decrease maximum number of caches by filtered ones result.setTotalCountGC(result.getTotalCountGC() - excluded); GCVote.loadRatings(includedCaches); return result; } @Nullable public Geocache getFirstCacheFromResult(final EnumSet<LoadFlag> loadFlags) { return CollectionUtils.isNotEmpty(geocodes) ? DataStore.loadCache(geocodes.iterator().next(), loadFlags) : null; } public Set<Geocache> getCachesFromSearchResult(final EnumSet<LoadFlag> loadFlags) { return DataStore.loadCaches(geocodes, loadFlags); } /** Add the geocode to the search. No cache is loaded into the CacheCache */ public boolean addGeocode(final String geocode) { if (StringUtils.isBlank(geocode)) { throw new IllegalArgumentException("geocode must not be blank"); } return geocodes.add(geocode); } /** Add the geocodes to the search. No caches are loaded into the CacheCache */ public boolean addGeocodes(final Set<String> geocodes) { return this.geocodes.addAll(geocodes); } /** Add the cache geocode to the search and store the cache in the CacheCache */ public void addAndPutInCache(@NonNull final Collection<Geocache> caches) { for (final Geocache geocache : caches) { addGeocode(geocache.getGeocode()); } DataStore.saveCaches(caches, EnumSet.of(SaveFlag.CACHE)); } public boolean isEmpty() { return geocodes.isEmpty(); } public boolean hasUnsavedCaches() { for (final String geocode : getGeocodes()) { if (!DataStore.isOffline(geocode, null)) { return true; } } return false; } public void addFilteredGeocodes(final Set<String> cachedMissingFromSearch) { filteredGeocodes.addAll(cachedMissingFromSearch); } public Set<String> getFilteredGeocodes() { return Collections.unmodifiableSet(filteredGeocodes); } public void addSearchResult(final SearchResult other) { if (other == null) { return; } addGeocodes(other.geocodes); addFilteredGeocodes(other.filteredGeocodes); if (StringUtils.isBlank(url)) { url = other.url; } // copy the GC total search results number to be able to use "More caches" button if (getTotalCountGC() == 0 && other.getTotalCountGC() != 0) { setViewstates(other.getViewstates()); setTotalCountGC(other.getTotalCountGC()); } } /** * execute the given connector request in parallel on all active connectors * * @param connectors * connectors to be considered in request * @param func * connector request */ public static <C extends IConnector> SearchResult parallelCombineActive(final Collection<C> connectors, final Function<C, SearchResult> func) { return Observable.fromIterable(connectors).flatMapMaybe(new Function<C, Maybe<SearchResult>>() { @Override public Maybe<SearchResult> apply(final C connector) { if (!connector.isActive()) { return Maybe.empty(); } return Maybe.fromCallable(new Callable<SearchResult>() { @Override public SearchResult call() throws Exception { try { return func.apply(connector); } catch (final Exception e) { Log.w("parallelCombineActive: swallowing error from connector " + connector, e); return null; } } }).subscribeOn(AndroidRxUtils.networkScheduler); } }).reduce(new SearchResult(), new BiFunction<SearchResult, SearchResult, SearchResult>() { @Override public SearchResult apply(final SearchResult searchResult, final SearchResult searchResult2) { searchResult.addSearchResult(searchResult2); return searchResult; } }).blockingGet(); } }