package cgeo.geocaching.connector; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.connector.capability.ISearchByCenter; import cgeo.geocaching.connector.capability.ISearchByFinder; import cgeo.geocaching.connector.capability.ISearchByKeyword; import cgeo.geocaching.connector.capability.ISearchByNextPage; import cgeo.geocaching.connector.capability.ISearchByOwner; import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.connector.ec.ECConnector; import cgeo.geocaching.connector.ga.GeocachingAustraliaConnector; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.MapTokens; import cgeo.geocaching.connector.ge.GeopeitusConnector; import cgeo.geocaching.connector.oc.OCApiConnector.ApiSupport; import cgeo.geocaching.connector.oc.OCApiLiveConnector; import cgeo.geocaching.connector.oc.OCCZConnector; import cgeo.geocaching.connector.oc.OCConnector; import cgeo.geocaching.connector.oc.OCDEConnector; import cgeo.geocaching.connector.su.GeocachingSuConnector; import cgeo.geocaching.connector.tc.TerraCachingConnector; import cgeo.geocaching.connector.trackable.GeokretyConnector; import cgeo.geocaching.connector.trackable.GeolutinsConnector; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.connector.trackable.TrackableTrackingCode; import cgeo.geocaching.connector.trackable.TravelBugConnector; import cgeo.geocaching.connector.trackable.UnknownTrackableConnector; import cgeo.geocaching.location.Viewport; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.utils.AndroidRxUtils; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import org.apache.commons.lang3.StringUtils; public final class ConnectorFactory { @NonNull public static final UnknownConnector UNKNOWN_CONNECTOR = new UnknownConnector(); @NonNull private static final Collection<IConnector> CONNECTORS = Collections.unmodifiableCollection(Arrays.<IConnector> asList( GCConnector.getInstance(), ECConnector.getInstance(), new OCDEConnector(), new OCCZConnector(), new OCApiLiveConnector("opencache.uk", "opencache.uk", false, "OK", "CC BY-NC-SA 2.5", R.string.oc_uk2_okapi_consumer_key, R.string.oc_uk2_okapi_consumer_secret, R.string.pref_connectorOCUKActive, R.string.pref_ocuk2_tokenpublic, R.string.pref_ocuk2_tokensecret, ApiSupport.current, "OC.UK"), new OCConnector("OpenCaching.NO/SE", "www.opencaching.se", false, "OS", "OC.NO"), new OCApiLiveConnector("opencaching.nl", "www.opencaching.nl", false, "OB", "CC BY-SA 3.0", R.string.oc_nl_okapi_consumer_key, R.string.oc_nl_okapi_consumer_secret, R.string.pref_connectorOCNLActive, R.string.pref_ocnl_tokenpublic, R.string.pref_ocnl_tokensecret, ApiSupport.current, "OC.NL"), new OCApiLiveConnector("opencaching.pl", "opencaching.pl", false, "OP", "CC BY-SA 3.0", R.string.oc_pl_okapi_consumer_key, R.string.oc_pl_okapi_consumer_secret, R.string.pref_connectorOCPLActive, R.string.pref_ocpl_tokenpublic, R.string.pref_ocpl_tokensecret, ApiSupport.current, "OC.PL"), new OCApiLiveConnector("opencaching.us", "www.opencaching.us", true, "OU", "CC BY-NC-SA 2.5", R.string.oc_us_okapi_consumer_key, R.string.oc_us_okapi_consumer_secret, R.string.pref_connectorOCUSActive, R.string.pref_ocus_tokenpublic, R.string.pref_ocus_tokensecret, ApiSupport.current, "OC.US"), new OCApiLiveConnector("opencaching.ro", "www.opencaching.ro", false, "OR", "CC BY-SA 3.0", R.string.oc_ro_okapi_consumer_key, R.string.oc_ro_okapi_consumer_secret, R.string.pref_connectorOCROActive, R.string.pref_ocro_tokenpublic, R.string.pref_ocro_tokensecret, ApiSupport.current, "OC.RO"), new GeocachingAustraliaConnector(), new GeopeitusConnector(), new TerraCachingConnector(), new WaymarkingConnector(), GeocachingSuConnector.getInstance(), UNKNOWN_CONNECTOR // the unknown connector MUST be the last one )); @NonNull public static final UnknownTrackableConnector UNKNOWN_TRACKABLE_CONNECTOR = new UnknownTrackableConnector(); @NonNull private static final Collection<TrackableConnector> TRACKABLE_CONNECTORS = Collections.unmodifiableCollection(Arrays.<TrackableConnector> asList( new GeokretyConnector(), new GeolutinsConnector(), TravelBugConnector.getInstance(), // travel bugs last, as their secret codes overlap with other connectors UNKNOWN_TRACKABLE_CONNECTOR // must be last )); @NonNull private static final Collection<ISearchByViewPort> searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class); @NonNull private static final Collection<ISearchByCenter> searchByCenterConns = getMatchingConnectors(ISearchByCenter.class); @NonNull private static final Collection<ISearchByNextPage> searchByNextPageConns = getMatchingConnectors(ISearchByNextPage.class); @NonNull private static final Collection<ISearchByKeyword> searchByKeywordConns = getMatchingConnectors(ISearchByKeyword.class); @NonNull private static final Collection<ISearchByOwner> SEARCH_BY_OWNER_CONNECTORS = getMatchingConnectors(ISearchByOwner.class); @NonNull private static final Collection<ISearchByFinder> SEARCH_BY_FINDER_CONNECTORS = getMatchingConnectors(ISearchByFinder.class); private static boolean forceRelog = false; // c:geo needs to log into cache providers public static boolean showLoginToast = true; //login toast shown just once. private ConnectorFactory() { // utility class } @NonNull @SuppressWarnings("unchecked") private static <T extends IConnector> Collection<T> getMatchingConnectors(final Class<T> clazz) { final List<T> matching = new ArrayList<>(); for (final IConnector connector : CONNECTORS) { if (clazz.isInstance(connector)) { matching.add((T) connector); } } return Collections.unmodifiableCollection(matching); } @NonNull public static Collection<IConnector> getConnectors() { return CONNECTORS; } @NonNull public static Collection<ISearchByCenter> getSearchByCenterConnectors() { return searchByCenterConns; } @NonNull public static Collection<ISearchByNextPage> getSearchByNextPageConnectors() { return searchByNextPageConns; } @NonNull public static Collection<ISearchByKeyword> getSearchByKeywordConnectors() { return searchByKeywordConns; } @NonNull public static Collection<ISearchByOwner> getSearchByOwnerConnectors() { return SEARCH_BY_OWNER_CONNECTORS; } @NonNull public static Collection<ISearchByFinder> getSearchByFinderConnectors() { return SEARCH_BY_FINDER_CONNECTORS; } @NonNull public static ILogin[] getActiveLiveConnectors() { final List<ILogin> liveConns = new ArrayList<>(); for (final IConnector conn : CONNECTORS) { if (conn instanceof ILogin && conn.isActive()) { liveConns.add((ILogin) conn); } } return liveConns.toArray(new ILogin[liveConns.size()]); } public static boolean canHandle(@Nullable final String geocode) { if (geocode == null) { return false; } if (isInvalidGeocode(geocode)) { return false; } for (final IConnector connector : CONNECTORS) { if (connector.canHandle(geocode)) { return true; } } return false; } /** * Get the connector handling all the operations available on this geocache. There is always a connector, it might * be the {@link UnknownConnector} if the geocache can't be identified. */ @NonNull public static IConnector getConnector(final Geocache cache) { return getConnector(cache.getGeocode()); } /** * Get a connector capability for the given geocache. This might be {@code null} if the connector does not support * the given capability. * * @return the connector cast to the requested capability or {@code null}. */ @Nullable public static <T extends IConnector> T getConnectorAs(@Nullable final Geocache cache, @NonNull final Class<T> capabilityClass) { if (cache == null) { return null; } final IConnector connector = getConnector(cache); if (capabilityClass.isInstance(connector)) { return capabilityClass.cast(connector); } return null; } @NonNull public static TrackableConnector getConnector(final Trackable trackable) { return getTrackableConnector(trackable.getGeocode()); } @NonNull public static TrackableConnector getTrackableConnector(final String geocode) { return getTrackableConnector(geocode, TrackableBrand.UNKNOWN); } @NonNull public static TrackableConnector getTrackableConnector(final String geocode, final TrackableBrand brand) { for (final TrackableConnector connector : TRACKABLE_CONNECTORS) { if (connector.canHandleTrackable(geocode, brand)) { return connector; } } return UNKNOWN_TRACKABLE_CONNECTOR; // avoid null checks by returning a non implementing connector } /** * Get the list of active generic trackable connectors * * @return the list of actives connectors. */ public static List<TrackableConnector> getGenericTrackablesConnectors() { final List<TrackableConnector> trackableConnectors = new ArrayList<>(); for (final TrackableConnector connector : TRACKABLE_CONNECTORS) { if (connector.isActive()) { trackableConnectors.add(connector); } } return trackableConnectors; } /** * Get the list of active generic trackable connectors with support logging and currently connected * * @return the list of actives connectors supporting logging. */ public static List<TrackableConnector> getLoggableGenericTrackablesConnectors() { final List<TrackableConnector> trackableConnectors = new ArrayList<>(); for (final TrackableConnector connector : getGenericTrackablesConnectors()) { if (connector.isGenericLoggable() && connector.isRegistered()) { trackableConnectors.add(connector); } } return trackableConnectors; } @NonNull public static IConnector getConnector(final String geocodeInput) { // this may come from user input final String geocode = StringUtils.trim(geocodeInput); if (geocode == null) { return UNKNOWN_CONNECTOR; } if (isInvalidGeocode(geocode)) { return UNKNOWN_CONNECTOR; } for (final IConnector connector : CONNECTORS) { if (connector.canHandle(geocode)) { return connector; } } // in case of errors, take UNKNOWN to avoid null checks everywhere return UNKNOWN_CONNECTOR; } /** * Obtain the connector by it's name. * If connector is not found, return UNKNOWN_CONNECTOR. * * @param connectorName * connector name String * @return * The connector matching name */ @NonNull public static IConnector getConnectorByName(final String connectorName) { for (final IConnector connector : CONNECTORS) { if (StringUtils.equals(connectorName, connector.getName())) { return connector; } } // in case of errors, take UNKNOWN to avoid null checks everywhere return UNKNOWN_CONNECTOR; } private static boolean isInvalidGeocode(final String geocode) { return StringUtils.isBlank(geocode) || !Character.isLetterOrDigit(geocode.charAt(0)); } /** @see ISearchByViewPort#searchByViewport */ @NonNull public static SearchResult searchByViewport(@NonNull final Viewport viewport, @Nullable final MapTokens tokens) { return SearchResult.parallelCombineActive(searchByViewPortConns, new Function<ISearchByViewPort, SearchResult>() { @Override public SearchResult apply(final ISearchByViewPort connector) { return connector.searchByViewport(viewport, tokens); } }); } @Nullable public static String getGeocodeFromURL(@Nullable final String url) { if (url == null) { return null; } for (final IConnector connector : CONNECTORS) { final String geocode = connector.getGeocodeFromUrl(url); if (StringUtils.isNotBlank(geocode)) { return StringUtils.upperCase(geocode); } } return null; } @NonNull public static Collection<TrackableConnector> getTrackableConnectors() { return TRACKABLE_CONNECTORS; } /** * Get trackable geocode from an URL. * * @return * the geocode, {@code null} if the URL cannot be decoded */ @Nullable public static String getTrackableFromURL(final String url) { if (url == null) { return null; } for (final TrackableConnector connector : TRACKABLE_CONNECTORS) { final String geocode = connector.getTrackableCodeFromUrl(url); if (StringUtils.isNotBlank(geocode)) { return geocode; } } return null; } /** * Get trackable Tracking Code from an URL. * * @return * the TrackableTrackingCode object, {@code null} if the URL cannot be decoded */ @NonNull public static TrackableTrackingCode getTrackableTrackingCodeFromURL(final String url) { if (url == null) { return TrackableTrackingCode.EMPTY; } for (final TrackableConnector connector : TRACKABLE_CONNECTORS) { final String trackableCode = connector.getTrackableTrackingCodeFromUrl(url); if (StringUtils.isNotBlank(trackableCode)) { return new TrackableTrackingCode(trackableCode, connector.getBrand()); } } return TrackableTrackingCode.EMPTY; } /** * Load a trackable. * * We query all the connectors that can handle the trackable in parallel as well as the local storage. * We return the first positive result coming from a connector, or, if none, the result of loading from * the local storage. * * @param geocode * trackable geocode * @param guid * trackable guid * @param id * trackable id * @param brand * trackable brand * @return * The Trackable observable */ public static Maybe<Trackable> loadTrackable(final String geocode, final String guid, final String id, final TrackableBrand brand) { if (StringUtils.isEmpty(geocode)) { // Only solution is GC search by uid return Maybe.fromCallable(new Callable<Trackable>() { @Override public Trackable call() { return TravelBugConnector.getInstance().searchTrackable(geocode, guid, id); } }).subscribeOn(AndroidRxUtils.networkScheduler); } final Observable<Trackable> fromNetwork = Observable.fromIterable(getTrackableConnectors()).filter(new Predicate<TrackableConnector>() { @Override public boolean test(final TrackableConnector trackableConnector) { return trackableConnector.canHandleTrackable(geocode, brand); } }).flatMapMaybe(new Function<TrackableConnector, Maybe<Trackable>>() { @Override public Maybe<Trackable> apply(final TrackableConnector trackableConnector) { return Maybe.fromCallable(new Callable<Trackable>() { @Override public Trackable call() { return trackableConnector.searchTrackable(geocode, guid, id); } }).subscribeOn(AndroidRxUtils.networkScheduler); } }); final Maybe<Trackable> fromLocalStorage = Maybe.fromCallable(new Callable<Trackable>() { @Override public Trackable call() { return DataStore.loadTrackable(geocode); } }).subscribeOn(Schedulers.io()); return fromNetwork.firstElement().switchIfEmpty(fromLocalStorage); } /** * Check if cgeo must relog even if already logged in. * * @return {@code true} if it is necessary to relog */ public static boolean mustRelog() { final boolean mustLogin = forceRelog; forceRelog = false; return mustLogin; } /** * Force cgeo to relog when reaching the main activity. */ public static void forceRelog() { forceRelog = true; } }