OkHttpJsonApi#getMediaList migrated to retrofit (#3054)

* Migrated OkHttpJsonApi#getMediaList partially to MediaClient#getCategoryImages

* Migrated rest of OkHttpJsonApi#getMediaList functionality to MediaClient#getCategoryImages

* Removed unused code and tests

* Fixed small bug

* Added tests

* Removed unused CategoryImageController
This commit is contained in:
Ilgaz Er 2019-07-06 14:25:57 +03:00 committed by Vivek Maskara
parent d5198be3e3
commit 97122296dd
9 changed files with 153 additions and 331 deletions

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons.category;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Single;
@Singleton
public class CategoryImageController {
private OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
public CategoryImageController(OkHttpJsonApiClient okHttpJsonApiClient) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
/**
* Takes a category name as input and calls the API to get a list of images for that category
* @param categoryName
* @return
*/
public Single<List<Media>> getCategoryImages(String categoryName) {
return okHttpJsonApiClient.getMediaList("category", categoryName);
}
}

View file

@ -27,6 +27,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -56,7 +57,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
private boolean isLoading = true;
private String categoryName = null;
@Inject CategoryImageController controller;
@Inject MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore categoryKvStore;
@ -116,7 +117,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
isLoading = true;
progressBar.setVisibility(VISIBLE);
compositeDisposable.add(controller.getCategoryImages(categoryName)
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -222,7 +223,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
}
progressBar.setVisibility(VISIBLE);
compositeDisposable.add(controller.getCategoryImages(categoryName)
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)

View file

@ -85,7 +85,6 @@ public class NetworkingModule {
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
BuildConfig.WIKIMEDIA_API_HOST,
defaultKvStore,
gson);
}

View file

@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@ -32,18 +31,11 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static android.view.View.GONE;
@ -69,7 +61,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Inject RecentSearchesDao recentSearchesDao;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore defaultKvStore;
@ -148,7 +140,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
queryList.clear();
imagesAdapter.clear();
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -165,7 +157,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -228,7 +220,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
imagesNotFoundView.setText(getString(R.string.images_not_found, query));
imagesNotFoundView.setText(getString(R.string.images_not_found));
}
/**

View file

@ -1,9 +1,19 @@
package fr.free.nrw.commons.media;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import io.reactivex.Observable;
import io.reactivex.Single;
/**
@ -14,9 +24,13 @@ public class MediaClient {
private final MediaInterface mediaInterface;
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore;
@Inject
public MediaClient(MediaInterface mediaInterface) {
this.mediaInterface = mediaInterface;
this.continuationStore = new HashMap<>();
}
/**
@ -43,4 +57,51 @@ public class MediaClient {
.query().allImages().size() > 0)
.singleOrError();
}
/**
* This method takes the category as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param category the search category. Must start with "Category:"
* @return
*/
public Single<List<Media>> getMediaListFromCategory(String category) {
return responseToMediaList(
continuationStore.containsKey("category_" + category) ?
mediaInterface.getMediaListFromCategory(category, 10, continuationStore.get("category_" + category)) : //if true
mediaInterface.getMediaListFromCategory(category, 10, Collections.emptyMap()),
"category_" + category); //if false
}
/**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param keyword the search keyword
* @return
*/
public Single<List<Media>> getMediaListFromSearch(String keyword) {
return responseToMediaList(
continuationStore.containsKey("search_" + keyword) ?
mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
"search_" + keyword);
}
private Single<List<Media>> responseToMediaList(Observable<MwQueryResponse> response, String key) {
return response.flatMap(mwQueryResponse -> {
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().pages()) {
return Observable.empty();
}
continuationStore.put(key, mwQueryResponse.continuation());
return Observable.fromIterable(mwQueryResponse.query().pages());
})
.map(Media::from)
.collect(ArrayList<Media>::new, List::add);
}
}

View file

@ -1,10 +1,14 @@
package fr.free.nrw.commons.media;
import org.jetbrains.annotations.NotNull;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
/**
* Interface for interacting with Commons media related APIs
@ -12,6 +16,7 @@ import retrofit2.http.Query;
public interface MediaInterface {
/**
* Checks if a page exists or not.
*
* @param title the title of the page to be checked
* @return
*/
@ -20,9 +25,42 @@ public interface MediaInterface {
/**
* Check if file exists
*
* @param aisha1 the SHA of the media file to be checked
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2&list=allimages")
Observable<MwQueryResponse> checkFileExistsUsingSha(@Query("aisha1") String aisha1);
/**
* This method retrieves a list of Media objects filtered using image generator query
*
* @param category the category name. Must start with "Category:"
* @param itemLimit how many images are returned
* @param continuation the continuation string from the previous query or empty map
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters
"&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + //Media property parameters
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
"|Artist|LicenseShortName|LicenseUrl")
Observable<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation);
/**
* This method retrieves a list of Media objects filtered using image generator query
*
* @param keyword the searched keyword
* @param itemLimit how many images are returned
* @param continuation the continuation string from the previous query
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=search&gsrwhat=text&gsrnamespace=6" + //Search parameters
"&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + //Media property parameters
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
"|Artist|LicenseShortName|LicenseUrl")
Observable<MwQueryResponse> getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @QueryMap Map<String, String> continuation);
}

View file

@ -46,15 +46,11 @@ import timber.log.Timber;
public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
public static final Type mapType = new TypeToken<Map<String, String>>() {
}.getType();
private final OkHttpClient okHttpClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final String commonsBaseUrl;
private final JsonKvStore defaultKvStore;
private Gson gson;
@ -64,14 +60,12 @@ public class OkHttpJsonApiClient {
String sparqlQueryUrl,
String campaignsUrl,
String commonsBaseUrl,
JsonKvStore defaultKvStore,
Gson gson) {
this.okHttpClient = okHttpClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.commonsBaseUrl = commonsBaseUrl;
this.defaultKvStore = defaultKvStore;
this.gson = gson;
}
@ -284,6 +278,8 @@ public class OkHttpJsonApiClient {
});
}
/**
* Whenever imageInfo is fetched, these common properties can be specified for the API call
* https://www.mediawiki.org/wiki/API:Imageinfo
@ -304,124 +300,4 @@ public class OkHttpJsonApiClient {
return builder;
}
/**
* This method takes the keyword and queryType as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
* @param queryType queryType can be "search" OR "category"
* @param keyword the search keyword. Can be either category name or search query
* @return
*/
@Nullable
public Single<List<Media>> getMediaList(String queryType, String keyword) {
HttpUrl.Builder urlBuilder = HttpUrl
.parse(commonsBaseUrl)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("format", "json")
.addQueryParameter("formatversion", "2");
if (queryType.equals("search")) {
appendSearchParam(keyword, urlBuilder);
} else {
appendCategoryParams(keyword, urlBuilder);
}
appendQueryContinueValues(keyword, urlBuilder);
Request request = new Request.Builder()
.url(appendMediaProperties(urlBuilder).build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
List<Media> mediaList = new ArrayList<>();
if (response.body() != null && response.isSuccessful()) {
String json = response.body().string();
MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class);
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().pages()) {
return mediaList;
}
putContinueValues(keyword, mwQueryResponse.continuation());
List<MwQueryPage> pages = mwQueryResponse.query().pages();
for (MwQueryPage page : pages) {
Media media = Media.from(page);
if (media != null) {
mediaList.add(media);
}
}
}
return mediaList;
});
}
/**
* Append params for search query.
*
* @param query the search query to be sent to the API
* @param urlBuilder builder for HttpUrl
*/
private void appendSearchParam(String query, HttpUrl.Builder urlBuilder) {
urlBuilder.addQueryParameter("generator", "search")
.addQueryParameter("gsrwhat", "text")
.addQueryParameter("gsrnamespace", "6")
.addQueryParameter("gsrlimit", "25")
.addQueryParameter("gsrsearch", query);
}
/**
* It takes a urlBuilder and appends all the continue values as query parameters
*
* @param query
* @param urlBuilder
*/
private void appendQueryContinueValues(String query, HttpUrl.Builder urlBuilder) {
Map<String, String> continueValues = getContinueValues(query);
if (continueValues != null && continueValues.size() > 0) {
for (Map.Entry<String, String> entry : continueValues.entrySet()) {
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
}
}
}
/**
* Append parameters for category image generator
*
* @param categoryName name of the category
* @param urlBuilder HttpUrl builder
*/
private void appendCategoryParams(String categoryName, HttpUrl.Builder urlBuilder) {
urlBuilder.addQueryParameter("generator", "categorymembers")
.addQueryParameter("gcmtype", "file")
.addQueryParameter("gcmtitle", categoryName)
.addQueryParameter("gcmsort", "timestamp")//property to sort by;timestamp
.addQueryParameter("gcmdir", "desc")//in which direction to sort;descending
.addQueryParameter("gcmlimit", "10");
}
/**
* Stores the continue values for action=query
* These values are sent to the server in the subsequent call to fetch results after this point
*
* @param keyword
* @param values
*/
private void putContinueValues(String keyword, Map<String, String> values) {
defaultKvStore.putJson("query_continue_" + keyword, values);
}
/**
* Retrieves a map of continue values from shared preferences.
* These values are appended to the next API call
*
* @param keyword
* @return
*/
private Map<String, String> getContinueValues(String keyword) {
return defaultKvStore.getJson("query_continue_" + keyword, mapType);
}
}