Convert wikidata/mwapi to kotlin (part 4) (#6010)

* Convert ImageDetails to kotlin

* Convert MwException/MwServiceError to kotlin

* Convert ListUserResponse to kotlin

* Convert MwPostResponse to kotlin

* Convert MwQueryResponse to kotlin

* Convert MwQueryResult to kotlin

* Convert MwQueryPage to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Paul Hawke 2024-12-09 21:18:42 -06:00 committed by GitHub
parent 56ada36b83
commit 73311970c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 474 additions and 615 deletions

View file

@ -237,7 +237,7 @@ class LoginClient(
.subscribe({ response: MwQueryResponse? -> .subscribe({ response: MwQueryResponse? ->
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0 loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
loginResult.groups = loginResult.groups =
response?.query()?.getUserResponse(userName)?.groups ?: emptySet() response?.query()?.getUserResponse(userName)?.getGroups() ?: emptySet()
cb.success(loginResult) cb.success(loginResult)
}, { caught: Throwable -> }, { caught: Throwable ->
Timber.e(caught, "Login succeeded but getting group information failed. ") Timber.e(caught, "Login succeeded but getting group information failed. ")

View file

@ -938,7 +938,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
Observable.defer { Observable.defer {
thanksClient.thank( thanksClient.thank(
firstRevision.revisionId firstRevision.revisionId()
) )
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())

View file

@ -201,7 +201,7 @@ class ReviewActivity : BaseActivity() {
val caption = getString( val caption = getString(
R.string.review_is_uploaded_by, R.string.review_is_uploaded_by,
fileName, fileName,
revision.user revision.user()
) )
binding.tvImageCaption.text = caption binding.tvImageCaption.text = caption
binding.pbReviewImage.visibility = View.GONE binding.pbReviewImage.visibility = View.GONE

View file

@ -12,7 +12,6 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import java.util.ArrayList import java.util.ArrayList
import java.util.concurrent.Callable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
@ -27,7 +26,6 @@ import fr.free.nrw.commons.delete.DeleteHelper
import fr.free.nrw.commons.di.ApplicationlessInjection import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.utils.ViewUtil import fr.free.nrw.commons.utils.ViewUtil
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.ObservableSource
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
@ -175,7 +173,7 @@ class ReviewController @Inject constructor(
if (firstRevision == null) return if (firstRevision == null) return
Observable.defer { Observable.defer {
thanksClient.thank(firstRevision!!.revisionId) thanksClient.thank(firstRevision!!.revisionId())
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View file

@ -34,7 +34,7 @@ class ReviewHelper
reviewInterface reviewInterface
.getRecentChanges() .getRecentChanges()
.map { it.query()?.pages() } .map { it.query()?.pages() }
.map(MutableList<MwQueryPage>::shuffled) .map { it.shuffled() }
.flatMapIterable { changes: List<MwQueryPage>? -> changes } .flatMapIterable { changes: List<MwQueryPage>? -> changes }
.filter { isChangeReviewable(it) } .filter { isChangeReviewable(it) }

View file

@ -3,18 +3,15 @@ package fr.free.nrw.commons.review
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.databinding.FragmentReviewImageBinding import fr.free.nrw.commons.databinding.FragmentReviewImageBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import java.util.ArrayList
import javax.inject.Inject import javax.inject.Inject
@ -126,7 +123,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
enableButtons() enableButtons()
question = getString(R.string.review_thanks) question = getString(R.string.review_thanks)
user = reviewActivity.reviewController.firstRevision?.user user = reviewActivity.reviewController.firstRevision?.user()
?: savedInstanceState?.getString(SAVED_USER) ?: savedInstanceState?.getString(SAVED_USER)
//if the user is null because of whatsoever reason, review will not be sent anyways //if the user is null because of whatsoever reason, review will not be sent anyways

View file

@ -270,7 +270,7 @@ class UploadClient
if (uploadResult.upload == null) { if (uploadResult.upload == null) {
val exception = gson.fromJson(uploadResponse, MwException::class.java) val exception = gson.fromJson(uploadResponse, MwException::class.java)
Timber.e(exception, "Error in uploading file from stash") Timber.e(exception, "Error in uploading file from stash")
throw Exception(exception.getErrorCode()) throw Exception(exception.errorCode)
} }
uploadResult.upload uploadResult.upload
} }

View file

@ -1,17 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
public class ImageDetails {
private String name;
private String title;
@NonNull public String getName() {
return name;
}
@NonNull public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,6 @@
package fr.free.nrw.commons.wikidata.mwapi
class ImageDetails {
val name: String? = null
val title: String? = null
}

View file

@ -1,26 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class ListUserResponse {
@SerializedName("name") @Nullable private String name;
private long userid;
@Nullable private List<String> groups;
@Nullable public String name() {
return name;
}
@NonNull public Set<String> getGroups() {
return groups != null ? new ArraySet<>(groups) : Collections.emptySet();
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.wikidata.mwapi
class ListUserResponse {
private val name: String? = null
private val userid: Long = 0
private val groups: List<String>? = null
fun name(): String? = name
fun getGroups(): Set<String> =
groups?.toSet() ?: emptySet()
}

View file

@ -1,44 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
import java.util.List;
public class MwException extends RuntimeException {
@Nullable private final MwServiceError error;
@Nullable private final List<MwServiceError> errors;
public MwException(@Nullable MwServiceError error,
@Nullable final List<MwServiceError> errors) {
this.error = error;
this.errors = errors;
}
public String getErrorCode() {
if(error!=null) {
return error.getCode();
}
return errors != null ? errors.get(0).getCode() : null;
}
@Nullable public MwServiceError getError() {
return error;
}
@Nullable
public String getTitle() {
if (error != null) {
return error.getTitle();
}
return errors != null ? errors.get(0).getTitle() : null;
}
@Override
@Nullable
public String getMessage() {
if (error != null) {
return error.getDetails();
}
return errors != null ? errors.get(0).getDetails() : null;
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.wikidata.mwapi
class MwException(
val error: MwServiceError?,
private val errors: List<MwServiceError>?
) : RuntimeException() {
val errorCode: String?
get() = error?.code ?: errors?.get(0)?.code
val title: String?
get() = error?.title ?: errors?.get(0)?.title
override val message: String?
get() = error?.details ?: errors?.get(0)?.details
}

View file

@ -1,16 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
public class MwPostResponse extends MwResponse {
private int success;
public boolean success(@Nullable String result) {
return "success".equals(result);
}
public int getSuccessVal() {
return success;
}
}

View file

@ -0,0 +1,9 @@
package fr.free.nrw.commons.wikidata.mwapi
open class MwPostResponse : MwResponse() {
val successVal: Int = 0
fun success(result: String?): Boolean =
"success" == result
}

View file

@ -1,229 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo;
import fr.free.nrw.commons.wikidata.model.BaseModel;
import java.util.Collections;
import java.util.List;
/**
* A class representing a standard page object as returned by the MediaWiki API.
*/
public class MwQueryPage extends BaseModel {
private int pageid;
private int index;
@NonNull private String title;
@NonNull private CategoryInfo categoryinfo;
@Nullable private List<Revision> revisions;
@SerializedName("fileusage") @Nullable private List<FileUsage> fileUsages;
@SerializedName("globalusage") @Nullable private List<GlobalUsage> globalUsages;
@Nullable private List<Coordinates> coordinates;
@Nullable private List<Category> categories;
@Nullable private Thumbnail thumbnail;
@Nullable private String description;
@SerializedName("imageinfo") @Nullable private List<ImageInfo> imageInfo;
@Nullable private String redirectFrom;
@Nullable private String convertedFrom;
@Nullable private String convertedTo;
@NonNull public String title() {
return title;
}
@NonNull public CategoryInfo categoryInfo() {
return categoryinfo;
}
public int index() {
return index;
}
@Nullable public List<Revision> revisions() {
return revisions;
}
@Nullable public List<Category> categories() {
return categories;
}
@Nullable public List<Coordinates> coordinates() {
// TODO: Handle null values in lists during deserialization, perhaps with a new
// @RequiredElements annotation and corresponding TypeAdapter
if (coordinates != null) {
coordinates.removeAll(Collections.singleton(null));
}
return coordinates;
}
public int pageId() {
return pageid;
}
@Nullable public String thumbUrl() {
return thumbnail != null ? thumbnail.source() : null;
}
@Nullable public String description() {
return description;
}
@Nullable public ImageInfo imageInfo() {
return imageInfo != null ? imageInfo.get(0) : null;
}
public void redirectFrom(@Nullable String from) {
redirectFrom = from;
}
public void convertedFrom(@Nullable String from) {
convertedFrom = from;
}
public void convertedTo(@Nullable String to) {
convertedTo = to;
}
public void appendTitleFragment(@Nullable String fragment) {
title += "#" + fragment;
}
public boolean checkWhetherFileIsUsedInWikis() {
if (globalUsages != null && globalUsages.size() > 0) {
return true;
}
if (fileUsages == null || fileUsages.size() == 0) {
return false;
}
final int totalCount = fileUsages.size();
/* Ignore usage under https://commons.wikimedia.org/wiki/User:Didym/Mobile_upload/
which has been a gallery of all of our uploads since 2014 */
for (final FileUsage fileUsage : fileUsages) {
if ( ! fileUsage.title().contains("User:Didym/Mobile upload")) {
return true;
}
}
return false;
}
public static class Revision {
@SerializedName("revid") private long revisionId;
private String user;
@SerializedName("contentformat") @NonNull private String contentFormat;
@SerializedName("contentmodel") @NonNull private String contentModel;
@SerializedName("timestamp") @NonNull private String timeStamp;
@NonNull private String content;
@NonNull public String content() {
return content;
}
@NonNull public String timeStamp() {
return StringUtils.defaultString(timeStamp);
}
public long getRevisionId() {
return revisionId;
}
@NonNull
public String getUser() {
return StringUtils.defaultString(user);
}
}
public static class Coordinates {
@Nullable private Double lat;
@Nullable private Double lon;
@Nullable public Double lat() {
return lat;
}
@Nullable public Double lon() {
return lon;
}
}
public static class CategoryInfo {
private boolean hidden;
private int size;
private int pages;
private int files;
private int subcats;
public boolean isHidden() {
return hidden;
}
}
static class Thumbnail {
private String source;
private int width;
private int height;
String source() {
return source;
}
}
public static class GlobalUsage {
@SerializedName("title") private String title;
@SerializedName("wiki")private String wiki;
@SerializedName("url") private String url;
public String getTitle() {
return title;
}
public String getWiki() {
return wiki;
}
public String getUrl() {
return url;
}
}
public static class FileUsage {
@SerializedName("pageid") private int pageid;
@SerializedName("ns") private int ns;
@SerializedName("title") private String title;
public int pageId() {
return pageid;
}
public int ns() {
return ns;
}
public String title() {
return title;
}
}
public static class Category {
private int ns;
@SuppressWarnings("unused,NullableProblems") @Nullable private String title;
private boolean hidden;
public int ns() {
return ns;
}
@NonNull public String title() {
return StringUtils.defaultString(title);
}
public boolean hidden() {
return hidden;
}
}
}

View file

@ -0,0 +1,186 @@
package fr.free.nrw.commons.wikidata.mwapi
import androidx.annotation.VisibleForTesting
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.wikidata.model.BaseModel
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage.GlobalUsage
/**
* A class representing a standard page object as returned by the MediaWiki API.
*/
class MwQueryPage : BaseModel() {
private val pageid = 0
private val index = 0
private var title: String? = null
private val categoryinfo: CategoryInfo? = null
private val revisions: List<Revision>? = null
@SerializedName("fileusage")
private val fileUsages: List<FileUsage>? = null
@SerializedName("globalusage")
private val globalUsages: List<GlobalUsage>? = null
private val coordinates: MutableList<Coordinates?>? = null
private val categories: List<Category>? = null
private val thumbnail: Thumbnail? = null
private val description: String? = null
@SerializedName("imageinfo")
private val imageInfo: List<ImageInfo>? = null
private var redirectFrom: String? = null
private var convertedFrom: String? = null
private var convertedTo: String? = null
fun title(): String = title!!
fun categoryInfo(): CategoryInfo = categoryinfo!!
fun index(): Int = index
fun revisions(): List<Revision>? = revisions
fun categories(): List<Category>? = categories
// TODO: Handle null values in lists during deserialization, perhaps with a new
// @RequiredElements annotation and corresponding TypeAdapter
fun coordinates(): List<Coordinates?>? = coordinates?.filterNotNull()
fun pageId(): Int = pageid
fun thumbUrl(): String? = thumbnail?.source()
fun description(): String? = description
fun imageInfo(): ImageInfo? = imageInfo?.get(0)
fun redirectFrom(from: String?) {
redirectFrom = from
}
fun convertedFrom(from: String?) {
convertedFrom = from
}
fun convertedTo(to: String?) {
convertedTo = to
}
fun appendTitleFragment(fragment: String?) {
title += "#$fragment"
}
fun checkWhetherFileIsUsedInWikis(): Boolean {
return checkWhetherFileIsUsedInWikis(globalUsages, fileUsages)
}
class Revision {
@SerializedName("revid")
private val revisionId: Long = 0
private val user: String? = null
@SerializedName("contentformat")
private val contentFormat: String? = null
@SerializedName("contentmodel")
private val contentModel: String? = null
@SerializedName("timestamp")
private val timeStamp: String? = null
private val content: String? = null
fun revisionId(): Long = revisionId
fun user(): String = user ?: ""
fun content(): String = content!!
fun timeStamp(): String = timeStamp ?: ""
}
class Coordinates {
private val lat: Double? = null
private val lon: Double? = null
fun lat(): Double? = lat
fun lon(): Double? = lon
}
class CategoryInfo {
val isHidden: Boolean = false
private val size = 0
private val pages = 0
private val files = 0
private val subcats = 0
}
internal class Thumbnail {
private val source: String? = null
private val width = 0
private val height = 0
fun source(): String? = source
}
class GlobalUsage {
@SerializedName("title")
val title: String? = null
@SerializedName("wiki")
val wiki: String? = null
@SerializedName("url")
val url: String? = null
}
class FileUsage {
@SerializedName("pageid")
private val pageid = 0
@SerializedName("ns")
private val ns = 0
@SerializedName("title")
private var title: String? = null
fun pageId(): Int = pageid
fun ns(): Int = ns
fun title(): String = title ?: ""
fun setTitle(value: String) {
title = value
}
}
class Category {
private val ns = 0
private val title: String? = null
private val hidden = false
fun ns(): Int = ns
fun title(): String = title ?: ""
fun hidden(): Boolean = hidden
}
}
@VisibleForTesting
fun checkWhetherFileIsUsedInWikis(
globalUsages: List<GlobalUsage>?,
fileUsages: List<MwQueryPage.FileUsage>?
): Boolean {
if (!globalUsages.isNullOrEmpty()) {
return true
}
if (fileUsages.isNullOrEmpty()) {
return false
}
/* Ignore usage under https://commons.wikimedia.org/wiki/User:Didym/Mobile_upload/
which has been a gallery of all of our uploads since 2014 */
return fileUsages.filterNot {
it.title().contains("User:Didym/Mobile upload")
}.isNotEmpty()
}

View file

@ -1,26 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class MwQueryResponse extends MwResponse {
@SerializedName("continue") @Nullable private Map<String, String> continuation;
@SerializedName("query") @Nullable private MwQueryResult query;
@Nullable public Map<String, String> continuation() {
return continuation;
}
@Nullable public MwQueryResult query() {
return query;
}
public boolean success() {
return query != null;
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.wikidata.mwapi
import com.google.gson.annotations.SerializedName
class MwQueryResponse : MwResponse() {
@SerializedName("continue")
private val continuation: Map<String, String>? = null
private val query: MwQueryResult? = null
fun continuation(): Map<String, String>? = continuation
fun query(): MwQueryResult? = query
fun success(): Boolean = query != null
}

View file

@ -1,187 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo;
import fr.free.nrw.commons.wikidata.model.BaseModel;
import fr.free.nrw.commons.wikidata.model.notifications.Notification;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MwQueryResult extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@SerializedName("pages") @Nullable private List<MwQueryPage> pages;
@Nullable private List<Redirect> redirects;
@Nullable private List<ConvertedTitle> converted;
@SerializedName("userinfo") private UserInfo userInfo;
@Nullable private List<ListUserResponse> users;
@Nullable private Tokens tokens;
@Nullable private NotificationList notifications;
@SerializedName("allimages") @Nullable private List<ImageDetails> allImages;
@Nullable public List<MwQueryPage> pages() {
return pages;
}
@Nullable public MwQueryPage firstPage() {
if (pages != null && pages.size() > 0) {
return pages.get(0);
}
return null;
}
@NonNull
public List<ImageDetails> allImages() {
return allImages == null ? Collections.emptyList() : allImages;
}
@Nullable public UserInfo userInfo() {
return userInfo;
}
@Nullable public String csrfToken() {
return tokens != null ? tokens.csrf() : null;
}
@Nullable public String loginToken() {
return tokens != null ? tokens.login() : null;
}
@Nullable public NotificationList notifications() {
return notifications;
}
@Nullable public ListUserResponse getUserResponse(@NonNull String userName) {
if (users != null) {
for (ListUserResponse user : users) {
// MediaWiki user names are case sensitive, but the first letter is always capitalized.
if (StringUtils.capitalize(userName).equals(user.name())) {
return user;
}
}
}
return null;
}
@NonNull public Map<String, ImageInfo> images() {
Map<String, ImageInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages) {
if (page.imageInfo() != null) {
result.put(page.title(), page.imageInfo());
}
}
}
return result;
}
@Override
public void postProcess() {
resolveConvertedTitles();
resolveRedirectedTitles();
}
private void resolveRedirectedTitles() {
if (redirects == null || pages == null) {
return;
}
for (MwQueryPage page : pages) {
for (MwQueryResult.Redirect redirect : redirects) {
// TODO: Looks like result pages and redirects can also be matched on the "index"
// property. Confirm in the API docs and consider updating.
if (page.title().equals(redirect.to())) {
page.redirectFrom(redirect.from());
if (redirect.toFragment() != null) {
page.appendTitleFragment(redirect.toFragment());
}
}
}
}
}
private void resolveConvertedTitles() {
if (converted == null || pages == null) {
return;
}
// noinspection ConstantConditions
for (MwQueryResult.ConvertedTitle convertedTitle : converted) {
// noinspection ConstantConditions
for (MwQueryPage page : pages) {
if (page.title().equals(convertedTitle.to())) {
page.convertedFrom(convertedTitle.from());
page.convertedTo(convertedTitle.to());
}
}
}
}
private static class Redirect {
private int index;
@Nullable private String from;
@Nullable private String to;
@SerializedName("tofragment") @Nullable private String toFragment;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
@Nullable public String toFragment() {
return toFragment;
}
}
public static class ConvertedTitle {
@Nullable private String from;
@Nullable private String to;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
}
private static class Tokens {
@SuppressWarnings("unused,NullableProblems") @SerializedName("csrftoken")
@Nullable private String csrf;
@SuppressWarnings("unused,NullableProblems") @SerializedName("createaccounttoken")
@Nullable private String createAccount;
@SuppressWarnings("unused,NullableProblems") @SerializedName("logintoken")
@Nullable private String login;
@Nullable private String csrf() {
return csrf;
}
@Nullable private String createAccount() {
return createAccount;
}
@Nullable private String login() {
return login;
}
}
public static class NotificationList {
@Nullable
private List<Notification> list;
@Nullable
public List<Notification> list() {
return list;
}
}
}

View file

@ -0,0 +1,134 @@
package fr.free.nrw.commons.wikidata.mwapi
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter.PostProcessable
import fr.free.nrw.commons.wikidata.model.BaseModel
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo
import fr.free.nrw.commons.wikidata.model.notifications.Notification
import org.apache.commons.lang3.StringUtils
class MwQueryResult : BaseModel(), PostProcessable {
private val pages: List<MwQueryPage>? = null
private val redirects: List<Redirect>? = null
private val converted: List<ConvertedTitle>? = null
@SerializedName("userinfo")
private val userInfo: UserInfo? = null
private val users: List<ListUserResponse>? = null
private val tokens: Tokens? = null
private val notifications: NotificationList? = null
@SerializedName("allimages")
private val allImages: List<ImageDetails>? = null
fun pages(): List<MwQueryPage>? = pages
fun firstPage(): MwQueryPage? = pages?.firstOrNull()
fun allImages(): List<ImageDetails> = allImages ?: emptyList()
fun userInfo(): UserInfo? = userInfo
fun csrfToken(): String? = tokens?.csrf()
fun loginToken(): String? = tokens?.login()
fun notifications(): NotificationList? = notifications
fun getUserResponse(userName: String): ListUserResponse? =
users?.find { StringUtils.capitalize(userName) == it.name() }
fun images() = buildMap {
pages?.forEach { page ->
page.imageInfo()?.let {
put(page.title(), it)
}
}
}
override fun postProcess() {
resolveConvertedTitles()
resolveRedirectedTitles()
}
private fun resolveRedirectedTitles() {
if (redirects == null || pages == null) {
return
}
pages.forEach { page ->
redirects.forEach { redirect ->
// TODO: Looks like result pages and redirects can also be matched on the "index"
// property. Confirm in the API docs and consider updating.
if (page.title() == redirect.to()) {
page.redirectFrom(redirect.from())
if (redirect.toFragment() != null) {
page.appendTitleFragment(redirect.toFragment())
}
}
}
}
}
private fun resolveConvertedTitles() {
if (converted == null || pages == null) {
return
}
converted.forEach { convertedTitle ->
pages.forEach { page ->
if (page.title() == convertedTitle.to()) {
page.convertedFrom(convertedTitle.from())
page.convertedTo(convertedTitle.to())
}
}
}
}
private class Redirect {
private val index = 0
private val from: String? = null
private val to: String? = null
@SerializedName("tofragment")
private val toFragment: String? = null
fun to(): String? = to
fun from(): String? = from
fun toFragment(): String? = toFragment
}
class ConvertedTitle {
private val from: String? = null
private val to: String? = null
fun to(): String? = to
fun from(): String? = from
}
private class Tokens {
@SerializedName("csrftoken")
private val csrf: String? = null
@SerializedName("createaccounttoken")
private val createAccount: String? = null
@SerializedName("logintoken")
private val login: String? = null
fun csrf(): String? = csrf
fun createAccount(): String? = createAccount
fun login(): String? = login
}
class NotificationList {
private val list: List<Notification>? = null
fun list(): List<Notification>? = list
}
}

View file

@ -1,23 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
import fr.free.nrw.commons.wikidata.model.BaseModel;
import java.util.List;
public abstract class MwResponse extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings({"unused"}) @Nullable private List<MwServiceError> errors;
@SuppressWarnings("unused,NullableProblems") @SerializedName("servedby") @NonNull private String servedBy;
@Override
public void postProcess() {
if (errors != null && !errors.isEmpty()) {
throw new MwException(errors.get(0), errors);
}
}
}

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.wikidata.mwapi
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter.PostProcessable
import fr.free.nrw.commons.wikidata.model.BaseModel
abstract class MwResponse : BaseModel(), PostProcessable {
private val errors: List<MwServiceError>? = null
@SerializedName("servedby")
private val servedBy: String? = null
override fun postProcess() {
if (!errors.isNullOrEmpty()) {
throw MwException(errors[0], errors)
}
}
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.BaseModel;
/**
* Gson POJO for a MediaWiki API error.
*/
public class MwServiceError extends BaseModel {
@Nullable private String code;
@Nullable private String text;
@NonNull public String getTitle() {
return StringUtils.defaultString(code);
}
@NonNull public String getDetails() {
return StringUtils.defaultString(text);
}
@Nullable
public String getCode() {
return code;
}
}

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.wikidata.mwapi
import fr.free.nrw.commons.wikidata.model.BaseModel
import org.apache.commons.lang3.StringUtils
/**
* Gson POJO for a MediaWiki API error.
*/
class MwServiceError : BaseModel() {
val code: String? = null
private val text: String? = null
val title: String
get() = code ?: ""
val details: String
get() = text ?: ""
}

View file

@ -139,7 +139,7 @@ class ReviewControllerTest {
@Test @Test
fun testSendThanks() { fun testSendThanks() {
shadowOf(Looper.getMainLooper()).idle() shadowOf(Looper.getMainLooper()).idle()
whenever(firstRevision.revisionId).thenReturn(1) whenever(firstRevision.revisionId()).thenReturn(1)
Whitebox.setInternalState(controller, "firstRevision", firstRevision) Whitebox.setInternalState(controller, "firstRevision", firstRevision)
controller.sendThanks(activity) controller.sendThanks(activity)
assertEquals( assertEquals(

View file

@ -95,11 +95,11 @@ class ReviewHelperTest {
@Test @Test
fun getFirstRevisionOfFile() { fun getFirstRevisionOfFile() {
val rev1 = mock<MwQueryPage.Revision>() val rev1 = mock<MwQueryPage.Revision>()
whenever(rev1.user).thenReturn("TestUser") whenever(rev1.user()).thenReturn("TestUser")
whenever(rev1.revisionId).thenReturn(1L) whenever(rev1.revisionId()).thenReturn(1L)
val rev2 = mock<MwQueryPage.Revision>() val rev2 = mock<MwQueryPage.Revision>()
whenever(rev2.user).thenReturn("TestUser") whenever(rev2.user()).thenReturn("TestUser")
whenever(rev2.revisionId).thenReturn(2L) whenever(rev2.revisionId()).thenReturn(2L)
val page = setupMedia("Test.jpg", rev1, rev2) val page = setupMedia("Test.jpg", rev1, rev2)
whenever(mwQueryResult.firstPage()).thenReturn(page) whenever(mwQueryResult.firstPage()).thenReturn(page)
@ -107,7 +107,7 @@ class ReviewHelperTest {
val firstRevisionOfFile = reviewHelper.getFirstRevisionOfFile("Test.jpg").blockingFirst() val firstRevisionOfFile = reviewHelper.getFirstRevisionOfFile("Test.jpg").blockingFirst()
assertEquals(1, firstRevisionOfFile.revisionId) assertEquals(1, firstRevisionOfFile.revisionId())
} }
@Test @Test

View file

@ -0,0 +1,48 @@
package fr.free.nrw.commons.wikidata.mwapi
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class MwQueryPageTest {
private val didymUsage = MwQueryPage.FileUsage().apply {
setTitle("User:Didym/Mobile upload")
}
@Test
fun checkWhetherFileIsUsedInWikis_nullGlobalUsages() {
assertFalse(checkWhetherFileIsUsedInWikis(null, null))
}
@Test
fun checkWhetherFileIsUsedInWikis_emptyGlobalUsages() {
assertFalse(checkWhetherFileIsUsedInWikis(emptyList(), null))
}
@Test
fun checkWhetherFileIsUsedInWikis_emptyFileUsage() {
assertFalse(checkWhetherFileIsUsedInWikis(emptyList(), emptyList()))
}
@Test
fun checkWhetherFileIsUsedInWikis_singleGlobalUsages() {
assertTrue(checkWhetherFileIsUsedInWikis(listOf(MwQueryPage.GlobalUsage()), null))
}
@Test
fun checkWhetherFileIsUsedInWikis_singleFileUsageContainsDidym() {
assertFalse(checkWhetherFileIsUsedInWikis(null, listOf(didymUsage)))
}
@Test
fun checkWhetherFileIsUsedInWikis_didymIgnoredInList() {
assertTrue(
checkWhetherFileIsUsedInWikis(
null, listOf(
didymUsage, MwQueryPage.FileUsage().apply { setTitle("somewhere else") }
)
)
)
}
}