mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-29 13:53:54 +01:00
Convert mwapi/wikidata to kotlin (part 1) (#5991)
* Convert OkHttpJsonApiClient and CategoryApi to kotlin * Convert GsonUtil to kotlin * Convert WikidataConstants to kotlin * Convert WikidataEditListener to kotlin * Convert WikidataEditService to kotlin * work in progress * Convert RequiredFieldsCheckOnReadTypeAdapterFactory to kotlin * Converted type adapters * Convert WikiSiteTypeAdapter to kotlin * Fixed nullability
This commit is contained in:
parent
9dd504e560
commit
3777f18bf9
41 changed files with 1490 additions and 1746 deletions
|
|
@ -10,11 +10,10 @@ class CommonsServiceFactory(
|
|||
) {
|
||||
val builder: Retrofit.Builder by lazy {
|
||||
// All instances of retrofit share this configuration, but create it lazily
|
||||
Retrofit
|
||||
.Builder()
|
||||
Retrofit.Builder()
|
||||
.client(okHttpClient)
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonUtil.defaultGson))
|
||||
}
|
||||
|
||||
val retrofitCache: MutableMap<String, Retrofit> = mutableMapOf()
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory;
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue;
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||
import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter;
|
||||
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
|
||||
import fr.free.nrw.commons.wikidata.json.UriTypeAdapter;
|
||||
import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter;
|
||||
import fr.free.nrw.commons.wikidata.model.page.Namespace;
|
||||
|
||||
public final class GsonUtil {
|
||||
private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss";
|
||||
|
||||
private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder()
|
||||
.setDateFormat(DATE_FORMAT)
|
||||
.registerTypeAdapterFactory(DataValue.getPolymorphicTypeAdapter())
|
||||
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe())
|
||||
.registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe())
|
||||
.registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe())
|
||||
.registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory())
|
||||
.registerTypeAdapterFactory(new PostProcessingTypeAdapter());
|
||||
|
||||
private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create();
|
||||
|
||||
public static Gson getDefaultGson() {
|
||||
return DEFAULT_GSON;
|
||||
}
|
||||
|
||||
private GsonUtil() { }
|
||||
}
|
||||
29
app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt
Normal file
29
app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter
|
||||
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter
|
||||
import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory
|
||||
import fr.free.nrw.commons.wikidata.json.UriTypeAdapter
|
||||
import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue.Companion.polymorphicTypeAdapter
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||
import fr.free.nrw.commons.wikidata.model.page.Namespace
|
||||
|
||||
object GsonUtil {
|
||||
private const val DATE_FORMAT = "MMM dd, yyyy HH:mm:ss"
|
||||
|
||||
private val DEFAULT_GSON_BUILDER: GsonBuilder by lazy {
|
||||
GsonBuilder().setDateFormat(DATE_FORMAT)
|
||||
.registerTypeAdapterFactory(polymorphicTypeAdapter)
|
||||
.registerTypeHierarchyAdapter(Uri::class.java, UriTypeAdapter().nullSafe())
|
||||
.registerTypeHierarchyAdapter(Namespace::class.java, NamespaceTypeAdapter().nullSafe())
|
||||
.registerTypeAdapter(WikiSite::class.java, WikiSiteTypeAdapter().nullSafe())
|
||||
.registerTypeAdapterFactory(RequiredFieldsCheckOnReadTypeAdapterFactory())
|
||||
.registerTypeAdapterFactory(PostProcessingTypeAdapter())
|
||||
}
|
||||
|
||||
val defaultGson: Gson by lazy { DEFAULT_GSON_BUILDER.create() }
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
public class WikidataConstants {
|
||||
public static final String PLACE_OBJECT = "place";
|
||||
public static final String BOOKMARKS_ITEMS = "bookmarks.items";
|
||||
public static final String SELECTED_NEARBY_PLACE = "selected.nearby.place";
|
||||
public static final String SELECTED_NEARBY_PLACE_CATEGORY = "selected.nearby.place.category";
|
||||
|
||||
public static final String MW_API_PREFIX = "w/api.php?format=json&formatversion=2&errorformat=plaintext&";
|
||||
public static final String WIKIPEDIA_URL = "https://wikipedia.org/";
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
object WikidataConstants {
|
||||
const val PLACE_OBJECT: String = "place"
|
||||
const val BOOKMARKS_ITEMS: String = "bookmarks.items"
|
||||
const val SELECTED_NEARBY_PLACE: String = "selected.nearby.place"
|
||||
const val SELECTED_NEARBY_PLACE_CATEGORY: String = "selected.nearby.place.category"
|
||||
|
||||
const val MW_API_PREFIX: String = "w/api.php?format=json&formatversion=2&errorformat=plaintext&"
|
||||
const val WIKIPEDIA_URL: String = "https://wikipedia.org/"
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
public abstract class WikidataEditListener {
|
||||
|
||||
protected WikidataP18EditListener wikidataP18EditListener;
|
||||
|
||||
public abstract void onSuccessfulWikidataEdit();
|
||||
|
||||
public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) {
|
||||
this.wikidataP18EditListener = wikidataP18EditListener;
|
||||
}
|
||||
|
||||
public interface WikidataP18EditListener {
|
||||
void onWikidataEditSuccessful();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
abstract class WikidataEditListener {
|
||||
var authenticationStateListener: WikidataP18EditListener? = null
|
||||
|
||||
abstract fun onSuccessfulWikidataEdit()
|
||||
|
||||
interface WikidataP18EditListener {
|
||||
fun onWikidataEditSuccessful()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
/**
|
||||
* Listener for wikidata edits
|
||||
*/
|
||||
public class WikidataEditListenerImpl extends WikidataEditListener {
|
||||
|
||||
public WikidataEditListenerImpl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired
|
||||
*/
|
||||
@Override
|
||||
public void onSuccessfulWikidataEdit() {
|
||||
if (wikidataP18EditListener != null) {
|
||||
wikidataP18EditListener.onWikidataEditSuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
/**
|
||||
* Listener for wikidata edits
|
||||
*/
|
||||
class WikidataEditListenerImpl : WikidataEditListener() {
|
||||
/**
|
||||
* Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired
|
||||
*/
|
||||
override fun onSuccessfulWikidataEdit() {
|
||||
authenticationStateListener?.onWikidataEditSuccessful()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
|
||||
import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.gson.Gson;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.upload.UploadResult;
|
||||
import fr.free.nrw.commons.upload.WikidataItem;
|
||||
import fr.free.nrw.commons.upload.WikidataPlace;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue;
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue.ValueString;
|
||||
import fr.free.nrw.commons.wikidata.model.EditClaim;
|
||||
import fr.free.nrw.commons.wikidata.model.RemoveClaim;
|
||||
import fr.free.nrw.commons.wikidata.model.SnakPartial;
|
||||
import fr.free.nrw.commons.wikidata.model.StatementPartial;
|
||||
import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue;
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki
|
||||
* Apis to make the necessary calls, log the edits and fire listeners on successful edits
|
||||
*/
|
||||
@Singleton
|
||||
public class WikidataEditService {
|
||||
|
||||
public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
|
||||
|
||||
private final Context context;
|
||||
private final WikidataEditListener wikidataEditListener;
|
||||
private final JsonKvStore directKvStore;
|
||||
private final WikiBaseClient wikiBaseClient;
|
||||
private final WikidataClient wikidataClient;
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public WikidataEditService(final Context context,
|
||||
final WikidataEditListener wikidataEditListener,
|
||||
@Named("default_preferences") final JsonKvStore directKvStore,
|
||||
final WikiBaseClient wikiBaseClient,
|
||||
final WikidataClient wikidataClient, final Gson gson) {
|
||||
this.context = context;
|
||||
this.wikidataEditListener = wikidataEditListener;
|
||||
this.directKvStore = directKvStore;
|
||||
this.wikiBaseClient = wikiBaseClient;
|
||||
this.wikidataClient = wikidataClient;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call
|
||||
* to the wikibase API to set tag against the entity.
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private Observable<Boolean> addDepictsProperty(
|
||||
final String fileEntityId,
|
||||
final List<String> depictedItems
|
||||
) {
|
||||
final EditClaim data = editClaim(
|
||||
ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10")
|
||||
// Wikipedia:Sandbox (Q10)
|
||||
: depictedItems
|
||||
);
|
||||
|
||||
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
|
||||
.doOnNext(success -> {
|
||||
if (success) {
|
||||
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
|
||||
} else {
|
||||
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
|
||||
}
|
||||
})
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting DEPICTS property");
|
||||
ViewUtil.showLongToast(context, throwable.toString());
|
||||
})
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes depicts ID as a parameter and create a uploadable data with the Id
|
||||
* and send the data for POST operation
|
||||
*
|
||||
* @param fileEntityId ID of the file
|
||||
* @param depictedItems IDs of the selected depict item
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public Observable<Boolean> updateDepictsProperty(
|
||||
final String fileEntityId,
|
||||
final List<String> depictedItems
|
||||
) {
|
||||
final String entityId = PAGE_ID_PREFIX + fileEntityId;
|
||||
final List<String> claimIds = getDepictionsClaimIds(entityId);
|
||||
|
||||
final RemoveClaim data = removeClaim( /* Please consider removeClaim scenario for BetaDebug */
|
||||
ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10")
|
||||
// Wikipedia:Sandbox (Q10)
|
||||
: claimIds
|
||||
);
|
||||
|
||||
return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data))
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while removing existing claims for DEPICTS property");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
}).switchMap(success-> {
|
||||
if(success) {
|
||||
Timber.d("DEPICTS property was deleted successfully");
|
||||
return addDepictsProperty(fileEntityId, depictedItems);
|
||||
} else {
|
||||
Timber.d("Unable to delete DEPICTS property");
|
||||
return Observable.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private List<String> getDepictionsClaimIds(final String entityId) {
|
||||
return wikiBaseClient.getClaimIdsByProperty(entityId, WikidataProperties.DEPICTS.getPropertyName())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingFirst();
|
||||
}
|
||||
|
||||
private EditClaim editClaim(final List<String> entityIds) {
|
||||
return EditClaim.from(entityIds, WikidataProperties.DEPICTS.getPropertyName());
|
||||
}
|
||||
|
||||
private RemoveClaim removeClaim(final List<String> claimIds) {
|
||||
return RemoveClaim.from(claimIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success toast when the edit is made successfully
|
||||
*/
|
||||
private void showSuccessToast(final String wikiItemName) {
|
||||
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
|
||||
final String successMessage = String
|
||||
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
|
||||
ViewUtil.showLongToast(context, successMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
|
||||
* csrfTokenClient
|
||||
*
|
||||
* @param fileEntityId
|
||||
* @return
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
|
||||
final String captionValue) {
|
||||
return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue)
|
||||
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting Captions");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
})
|
||||
.map(mwPostResponse -> mwPostResponse != null);
|
||||
}
|
||||
|
||||
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
|
||||
if (response != null) {
|
||||
Timber.d("Caption successfully set, revision id = %s", response);
|
||||
} else {
|
||||
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName,
|
||||
final Map<String, String> captions) {
|
||||
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
|
||||
Timber
|
||||
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
|
||||
return null;
|
||||
}
|
||||
return addImageAndMediaLegends(wikidataPlace, fileName, captions);
|
||||
}
|
||||
|
||||
public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
|
||||
final Map<String, String> captions) {
|
||||
final SnakPartial p18 = new SnakPartial("value",
|
||||
WikidataProperties.IMAGE.getPropertyName(),
|
||||
new ValueString(fileName.replace("File:", "")));
|
||||
|
||||
final List<SnakPartial> snaks = new ArrayList<>();
|
||||
for (final Map.Entry<String, String> entry : captions.entrySet()) {
|
||||
snaks.add(new SnakPartial("value",
|
||||
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
|
||||
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
|
||||
}
|
||||
|
||||
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
|
||||
final StatementPartial claim = new StatementPartial(p18, "statement", "normal", id,
|
||||
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
|
||||
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
|
||||
|
||||
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
|
||||
}
|
||||
|
||||
public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
|
||||
if (revisionId != null) {
|
||||
if (wikidataEditListener != null) {
|
||||
wikidataEditListener.onSuccessfulWikidataEdit();
|
||||
}
|
||||
showSuccessToast(wikidataItem.getName());
|
||||
} else {
|
||||
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Boolean> addDepictionsAndCaptions(
|
||||
final UploadResult uploadResult,
|
||||
final Contribution contribution
|
||||
) {
|
||||
return wikiBaseClient.getFileEntityId(uploadResult)
|
||||
.doOnError(throwable -> {
|
||||
Timber
|
||||
.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
})
|
||||
.switchMap(fileEntityId -> {
|
||||
if (fileEntityId != null) {
|
||||
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
|
||||
return Observable.concat(
|
||||
depictionEdits(contribution, fileEntityId),
|
||||
captionEdits(contribution, fileEntityId)
|
||||
);
|
||||
} else {
|
||||
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
|
||||
return Observable.empty();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
|
||||
return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
|
||||
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
|
||||
}
|
||||
|
||||
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) {
|
||||
final List<String> depictIDs = new ArrayList<>();
|
||||
for (final WikidataItem wikidataItem :
|
||||
contribution.getDepictedItems()) {
|
||||
depictIDs.add(wikidataItem.getId());
|
||||
}
|
||||
return addDepictsProperty(fileEntityId.toString(), depictIDs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||
import fr.free.nrw.commons.upload.UploadResult
|
||||
import fr.free.nrw.commons.upload.WikidataItem
|
||||
import fr.free.nrw.commons.upload.WikidataPlace
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties.IMAGE
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties.MEDIA_LEGENDS
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue.MonoLingualText
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue.ValueString
|
||||
import fr.free.nrw.commons.wikidata.model.EditClaim
|
||||
import fr.free.nrw.commons.wikidata.model.RemoveClaim
|
||||
import fr.free.nrw.commons.wikidata.model.SnakPartial
|
||||
import fr.free.nrw.commons.wikidata.model.StatementPartial
|
||||
import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.util.Arrays
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
import java.util.Objects
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
/**
|
||||
* This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki
|
||||
* Apis to make the necessary calls, log the edits and fire listeners on successful edits
|
||||
*/
|
||||
@Singleton
|
||||
class WikidataEditService @Inject constructor(
|
||||
private val context: Context,
|
||||
private val wikidataEditListener: WikidataEditListener?,
|
||||
@param:Named("default_preferences") private val directKvStore: JsonKvStore,
|
||||
private val wikiBaseClient: WikiBaseClient,
|
||||
private val wikidataClient: WikidataClient, private val gson: Gson
|
||||
) {
|
||||
@SuppressLint("CheckResult")
|
||||
private fun addDepictsProperty(
|
||||
fileEntityId: String,
|
||||
depictedItems: List<String>
|
||||
): Observable<Boolean> {
|
||||
val data = EditClaim.from(
|
||||
if (isBetaFlavour) listOf("Q10") else depictedItems, DEPICTS.propertyName
|
||||
)
|
||||
|
||||
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
|
||||
.doOnNext { success: Boolean ->
|
||||
if (success) {
|
||||
Timber.d("DEPICTS property was set successfully for %s", fileEntityId)
|
||||
} else {
|
||||
Timber.d("Unable to set DEPICTS property for %s", fileEntityId)
|
||||
}
|
||||
}
|
||||
.doOnError { throwable: Throwable ->
|
||||
Timber.e(throwable, "Error occurred while setting DEPICTS property")
|
||||
showLongToast(context, throwable.toString())
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun updateDepictsProperty(
|
||||
fileEntityId: String?,
|
||||
depictedItems: List<String>
|
||||
): Observable<Boolean> {
|
||||
val entityId: String = PAGE_ID_PREFIX + fileEntityId
|
||||
val claimIds = getDepictionsClaimIds(entityId)
|
||||
|
||||
/* Please consider removeClaim scenario for BetaDebug */
|
||||
val data = RemoveClaim.from(if (isBetaFlavour) listOf("Q10") else claimIds)
|
||||
|
||||
return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data))
|
||||
.doOnError { throwable: Throwable? ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"Error occurred while removing existing claims for DEPICTS property"
|
||||
)
|
||||
showLongToast(
|
||||
context,
|
||||
context.getString(R.string.wikidata_edit_failure)
|
||||
)
|
||||
}.switchMap { success: Boolean ->
|
||||
if (success) {
|
||||
Timber.d("DEPICTS property was deleted successfully")
|
||||
return@switchMap addDepictsProperty(fileEntityId!!, depictedItems)
|
||||
} else {
|
||||
Timber.d("Unable to delete DEPICTS property")
|
||||
return@switchMap Observable.empty<Boolean>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun getDepictionsClaimIds(entityId: String): List<String> {
|
||||
return wikiBaseClient.getClaimIdsByProperty(entityId, DEPICTS.propertyName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingFirst()
|
||||
}
|
||||
|
||||
private fun showSuccessToast(wikiItemName: String) {
|
||||
val successStringTemplate = context.getString(R.string.successful_wikidata_edit)
|
||||
val successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName)
|
||||
showLongToast(context, successMessage)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun addCaption(
|
||||
fileEntityId: Long, languageCode: String,
|
||||
captionValue: String
|
||||
): Observable<Boolean> {
|
||||
return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue)
|
||||
.doOnNext { mwPostResponse: MwPostResponse? ->
|
||||
onAddCaptionResponse(
|
||||
fileEntityId,
|
||||
mwPostResponse
|
||||
)
|
||||
}
|
||||
.doOnError { throwable: Throwable? ->
|
||||
Timber.e(throwable, "Error occurred while setting Captions")
|
||||
showLongToast(
|
||||
context,
|
||||
context.getString(R.string.wikidata_edit_failure)
|
||||
)
|
||||
}
|
||||
.map(Objects::nonNull)
|
||||
}
|
||||
|
||||
private fun onAddCaptionResponse(fileEntityId: Long, response: MwPostResponse?) {
|
||||
if (response != null) {
|
||||
Timber.d("Caption successfully set, revision id = %s", response)
|
||||
} else {
|
||||
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId)
|
||||
}
|
||||
}
|
||||
|
||||
fun createClaim(
|
||||
wikidataPlace: WikidataPlace?, fileName: String,
|
||||
captions: Map<String, String>
|
||||
): Long? {
|
||||
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
|
||||
Timber.d(
|
||||
"Image location and nearby place location mismatched, so Wikidata item won't be edited"
|
||||
)
|
||||
return null
|
||||
}
|
||||
return addImageAndMediaLegends(wikidataPlace!!, fileName, captions)
|
||||
}
|
||||
|
||||
fun addImageAndMediaLegends(
|
||||
wikidataItem: WikidataItem, fileName: String,
|
||||
captions: Map<String, String>
|
||||
): Long {
|
||||
val p18 = SnakPartial(
|
||||
"value",
|
||||
IMAGE.propertyName,
|
||||
ValueString(fileName.replace("File:", ""))
|
||||
)
|
||||
|
||||
val snaks: MutableList<SnakPartial> = ArrayList()
|
||||
for ((key, value) in captions) {
|
||||
snaks.add(
|
||||
SnakPartial(
|
||||
"value",
|
||||
MEDIA_LEGENDS.propertyName, MonoLingualText(
|
||||
WikiBaseMonolingualTextValue(value!!, key!!)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val id = wikidataItem.id + "$" + UUID.randomUUID().toString()
|
||||
val claim = StatementPartial(
|
||||
p18, "statement", "normal", id, Collections.singletonMap<String, List<SnakPartial>>(
|
||||
MEDIA_LEGENDS.propertyName, snaks
|
||||
), Arrays.asList(MEDIA_LEGENDS.propertyName)
|
||||
)
|
||||
|
||||
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle()
|
||||
}
|
||||
|
||||
fun handleImageClaimResult(wikidataItem: WikidataItem, revisionId: Long?) {
|
||||
if (revisionId != null) {
|
||||
wikidataEditListener?.onSuccessfulWikidataEdit()
|
||||
showSuccessToast(wikidataItem.name)
|
||||
} else {
|
||||
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem)
|
||||
showLongToast(context, context.getString(R.string.wikidata_edit_failure))
|
||||
}
|
||||
}
|
||||
|
||||
fun addDepictionsAndCaptions(
|
||||
uploadResult: UploadResult,
|
||||
contribution: Contribution
|
||||
): Observable<Boolean> {
|
||||
return wikiBaseClient.getFileEntityId(uploadResult)
|
||||
.doOnError { throwable: Throwable? ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"Error occurred while getting EntityID to set DEPICTS property"
|
||||
)
|
||||
showLongToast(
|
||||
context,
|
||||
context.getString(R.string.wikidata_edit_failure)
|
||||
)
|
||||
}
|
||||
.switchMap { fileEntityId: Long? ->
|
||||
if (fileEntityId != null) {
|
||||
Timber.d("EntityId for image was received successfully: %s", fileEntityId)
|
||||
return@switchMap Observable.concat<Boolean>(
|
||||
depictionEdits(contribution, fileEntityId),
|
||||
captionEdits(contribution, fileEntityId)
|
||||
)
|
||||
} else {
|
||||
Timber.d("Error acquiring EntityId for image: %s", uploadResult)
|
||||
return@switchMap Observable.empty<Boolean>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun captionEdits(contribution: Contribution, fileEntityId: Long): Observable<Boolean> {
|
||||
return Observable.fromIterable(contribution.media.captions.entries)
|
||||
.concatMap { addCaption(fileEntityId, it.key, it.value) }
|
||||
}
|
||||
|
||||
private fun depictionEdits(
|
||||
contribution: Contribution,
|
||||
fileEntityId: Long
|
||||
): Observable<Boolean> = addDepictsProperty(fileEntityId.toString(), buildList {
|
||||
for ((_, _, _, _, _, _, id) in contribution.depictedItems) {
|
||||
add(id)
|
||||
}
|
||||
})
|
||||
|
||||
companion object {
|
||||
const val COMMONS_APP_TAG: String = "wikimedia-commons-app"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.model.page.Namespace;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class NamespaceTypeAdapter extends TypeAdapter<Namespace> {
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Namespace namespace) throws IOException {
|
||||
out.value(namespace.code());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Namespace read(JsonReader in) throws IOException {
|
||||
if (in.peek() == JsonToken.STRING) {
|
||||
// Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of
|
||||
// the code number. This introduces a backwards-compatible check for the string value.
|
||||
// TODO: remove after April 2017, when all older namespaces have been deserialized.
|
||||
return Namespace.valueOf(in.nextString());
|
||||
}
|
||||
return Namespace.of(in.nextInt());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import fr.free.nrw.commons.wikidata.model.page.Namespace
|
||||
import java.io.IOException
|
||||
|
||||
class NamespaceTypeAdapter : TypeAdapter<Namespace>() {
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, namespace: Namespace) {
|
||||
out.value(namespace.code().toLong())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): Namespace {
|
||||
if (reader.peek() == JsonToken.STRING) {
|
||||
// Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of
|
||||
// the code number. This introduces a backwards-compatible check for the string value.
|
||||
// TODO: remove after April 2017, when all older namespaces have been deserialized.
|
||||
return Namespace.valueOf(reader.nextString())
|
||||
}
|
||||
return Namespace.of(reader.nextInt())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PostProcessingTypeAdapter implements TypeAdapterFactory {
|
||||
public interface PostProcessable {
|
||||
void postProcess();
|
||||
}
|
||||
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||
|
||||
return new TypeAdapter<T>() {
|
||||
public void write(JsonWriter out, T value) throws IOException {
|
||||
delegate.write(out, value);
|
||||
}
|
||||
|
||||
public T read(JsonReader in) throws IOException {
|
||||
T obj = delegate.read(in);
|
||||
if (obj instanceof PostProcessable) {
|
||||
((PostProcessable)obj).postProcess();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.IOException
|
||||
|
||||
class PostProcessingTypeAdapter : TypeAdapterFactory {
|
||||
interface PostProcessable {
|
||||
fun postProcess()
|
||||
}
|
||||
|
||||
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
|
||||
val delegate = gson.getDelegateAdapter(this, type)
|
||||
|
||||
return object : TypeAdapter<T>() {
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: T) {
|
||||
delegate.write(out, value)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): T {
|
||||
val obj = delegate.read(reader)
|
||||
if (obj is PostProcessable) {
|
||||
(obj as PostProcessable).postProcess()
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.json.annotations.Required;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
|
||||
* missing fields annotated with @Required.
|
||||
*
|
||||
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
|
||||
* contain null elements after deserialization!
|
||||
*
|
||||
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
|
||||
* annotation and another corresponding TypeAdapter(Factory).
|
||||
*/
|
||||
public class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) {
|
||||
Class<?> rawType = typeToken.getRawType();
|
||||
Set<Field> requiredFields = collectRequiredFields(rawType);
|
||||
|
||||
if (requiredFields.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setFieldsAccessible(requiredFields, true);
|
||||
return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields);
|
||||
}
|
||||
|
||||
@NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) {
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
Set<Field> required = new ArraySet<>();
|
||||
for (Field field : fields) {
|
||||
if (field.isAnnotationPresent(Required.class)) {
|
||||
required.add(field);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(required);
|
||||
}
|
||||
|
||||
private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) {
|
||||
for (Field field : fields) {
|
||||
field.setAccessible(accessible);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Adapter<T> extends TypeAdapter<T> {
|
||||
@NonNull private final TypeAdapter<T> delegate;
|
||||
@NonNull private final Set<Field> requiredFields;
|
||||
|
||||
private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) {
|
||||
this.delegate = delegate;
|
||||
this.requiredFields = requiredFields;
|
||||
}
|
||||
|
||||
@Override public void write(JsonWriter out, T value) throws IOException {
|
||||
delegate.write(out, value);
|
||||
}
|
||||
|
||||
@Override @Nullable public T read(JsonReader in) throws IOException {
|
||||
T deserialized = delegate.read(in);
|
||||
return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null;
|
||||
}
|
||||
|
||||
private boolean allRequiredFieldsPresent(@NonNull T deserialized,
|
||||
@NonNull Set<Field> required) {
|
||||
for (Field field : required) {
|
||||
try {
|
||||
if (field.get(deserialized) == null) {
|
||||
return false;
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import fr.free.nrw.commons.wikidata.json.annotations.Required
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Field
|
||||
|
||||
/**
|
||||
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
|
||||
* missing fields annotated with @Required.
|
||||
*
|
||||
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
|
||||
* contain null elements after deserialization!
|
||||
*
|
||||
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
|
||||
* annotation and another corresponding TypeAdapter(Factory).
|
||||
*/
|
||||
class RequiredFieldsCheckOnReadTypeAdapterFactory : TypeAdapterFactory {
|
||||
override fun <T> create(gson: Gson, typeToken: TypeToken<T>): TypeAdapter<T>? {
|
||||
val rawType: Class<*> = typeToken.rawType
|
||||
val requiredFields = collectRequiredFields(rawType)
|
||||
|
||||
if (requiredFields.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (field in requiredFields) {
|
||||
field.isAccessible = true
|
||||
}
|
||||
|
||||
return Adapter(gson.getDelegateAdapter(this, typeToken), requiredFields)
|
||||
}
|
||||
|
||||
private fun collectRequiredFields(clazz: Class<*>): Set<Field> = buildSet {
|
||||
for (field in clazz.declaredFields) {
|
||||
if (field.isAnnotationPresent(Required::class.java)) add(field)
|
||||
}
|
||||
}
|
||||
|
||||
private class Adapter<T>(
|
||||
private val delegate: TypeAdapter<T>,
|
||||
private val requiredFields: Set<Field>
|
||||
) : TypeAdapter<T>() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: T?) =
|
||||
delegate.write(out, value)
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): T? =
|
||||
if (allRequiredFieldsPresent(delegate.read(reader), requiredFields))
|
||||
delegate.read(reader)
|
||||
else
|
||||
null
|
||||
|
||||
fun allRequiredFieldsPresent(deserialized: T, required: Set<Field>): Boolean {
|
||||
for (field in required) {
|
||||
try {
|
||||
if (field[deserialized] == null) return false
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonParseException(e)
|
||||
} catch (e: IllegalAccessException) {
|
||||
throw JsonParseException(e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.internal.Streams;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* Adapts values whose runtime type may differ from their declaration type. This
|
||||
* is necessary when a field's type is not the same type that GSON should create
|
||||
* when deserializing that field. For example, consider these types:
|
||||
* <pre> {@code
|
||||
* abstract class Shape {
|
||||
* int x;
|
||||
* int y;
|
||||
* }
|
||||
* class Circle extends Shape {
|
||||
* int radius;
|
||||
* }
|
||||
* class Rectangle extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
* class Diamond extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
* class Drawing {
|
||||
* Shape bottomShape;
|
||||
* Shape topShape;
|
||||
* }
|
||||
* }</pre>
|
||||
* <p>Without additional type information, the serialized JSON is ambiguous. Is
|
||||
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
|
||||
* {
|
||||
* "bottomShape": {
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }}</pre>
|
||||
* This class addresses this problem by adding type information to the
|
||||
* serialized JSON and honoring that type information when the JSON is
|
||||
* deserialized: <pre> {@code
|
||||
* {
|
||||
* "bottomShape": {
|
||||
* "type": "Diamond",
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "type": "Circle",
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }}</pre>
|
||||
* Both the type field name ({@code "type"}) and the type labels ({@code
|
||||
* "Rectangle"}) are configurable.
|
||||
*
|
||||
* <h3>Registering Types</h3>
|
||||
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
|
||||
* name to the {@link #of} factory method. If you don't supply an explicit type
|
||||
* field name, {@code "type"} will be used. <pre> {@code
|
||||
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
|
||||
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
|
||||
* }</pre>
|
||||
* Next register all of your subtypes. Every subtype must be explicitly
|
||||
* registered. This protects your application from injection attacks. If you
|
||||
* don't supply an explicit type label, the type's simple name will be used.
|
||||
* <pre> {@code
|
||||
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
|
||||
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
|
||||
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
|
||||
* }</pre>
|
||||
* Finally, register the type adapter factory in your application's GSON builder:
|
||||
* <pre> {@code
|
||||
* Gson gson = new GsonBuilder()
|
||||
* .registerTypeAdapterFactory(shapeAdapterFactory)
|
||||
* .create();
|
||||
* }</pre>
|
||||
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
|
||||
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
|
||||
* .registerSubtype(Rectangle.class)
|
||||
* .registerSubtype(Circle.class)
|
||||
* .registerSubtype(Diamond.class);
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Serialization and deserialization</h3>
|
||||
* In order to serialize and deserialize a polymorphic object,
|
||||
* you must specify the base type explicitly.
|
||||
* <pre> {@code
|
||||
* Diamond diamond = new Diamond();
|
||||
* String json = gson.toJson(diamond, Shape.class);
|
||||
* }</pre>
|
||||
* And then:
|
||||
* <pre> {@code
|
||||
* Shape shape = gson.fromJson(json, Shape.class);
|
||||
* }</pre>
|
||||
*/
|
||||
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
|
||||
private final Class<?> baseType;
|
||||
private final String typeFieldName;
|
||||
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
|
||||
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
|
||||
private final boolean maintainType;
|
||||
|
||||
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
|
||||
if (typeFieldName == null || baseType == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.baseType = baseType;
|
||||
this.typeFieldName = typeFieldName;
|
||||
this.maintainType = maintainType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter using for {@code baseType} using {@code
|
||||
* typeFieldName} as the type field name. Type field names are case sensitive.
|
||||
* {@code maintainType} flag decide if the type will be stored in pojo or not.
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
|
||||
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, maintainType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter using for {@code baseType} using {@code
|
||||
* typeFieldName} as the type field name. Type field names are case sensitive.
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
|
||||
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
|
||||
* the type field name.
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
|
||||
return new RuntimeTypeAdapterFactory<T>(baseType, "type", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers {@code type} identified by {@code label}. Labels are case
|
||||
* sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either {@code type} or {@code label}
|
||||
* have already been registered on this type adapter.
|
||||
*/
|
||||
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
|
||||
if (type == null || label == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
|
||||
throw new IllegalArgumentException("types and labels must be unique");
|
||||
}
|
||||
labelToSubtype.put(label, type);
|
||||
subtypeToLabel.put(type, label);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers {@code type} identified by its {@link Class#getSimpleName simple
|
||||
* name}. Labels are case sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either {@code type} or its simple name
|
||||
* have already been registered on this type adapter.
|
||||
*/
|
||||
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
|
||||
return registerSubtype(type, type.getSimpleName());
|
||||
}
|
||||
|
||||
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
|
||||
if (type.getRawType() != baseType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, TypeAdapter<?>> labelToDelegate
|
||||
= new LinkedHashMap<String, TypeAdapter<?>>();
|
||||
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
|
||||
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
|
||||
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
|
||||
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
|
||||
labelToDelegate.put(entry.getKey(), delegate);
|
||||
subtypeToDelegate.put(entry.getValue(), delegate);
|
||||
}
|
||||
|
||||
return new TypeAdapter<R>() {
|
||||
@Override public R read(JsonReader in) throws IOException {
|
||||
JsonElement jsonElement = Streams.parse(in);
|
||||
JsonElement labelJsonElement;
|
||||
if (maintainType) {
|
||||
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
|
||||
} else {
|
||||
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
|
||||
}
|
||||
|
||||
if (labelJsonElement == null) {
|
||||
throw new JsonParseException("cannot deserialize " + baseType
|
||||
+ " because it does not define a field named " + typeFieldName);
|
||||
}
|
||||
String label = labelJsonElement.getAsString();
|
||||
@SuppressWarnings("unchecked") // registration requires that subtype extends T
|
||||
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
|
||||
if (delegate == null) {
|
||||
|
||||
Log.e("RuntimeTypeAdapter", "cannot deserialize " + baseType + " subtype named "
|
||||
+ label + "; did you forget to register a subtype? " +jsonElement);
|
||||
return null;
|
||||
}
|
||||
return delegate.fromJsonTree(jsonElement);
|
||||
}
|
||||
|
||||
@Override public void write(JsonWriter out, R value) throws IOException {
|
||||
Class<?> srcType = value.getClass();
|
||||
String label = subtypeToLabel.get(srcType);
|
||||
@SuppressWarnings("unchecked") // registration requires that subtype extends T
|
||||
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
|
||||
if (delegate == null) {
|
||||
throw new JsonParseException("cannot serialize " + srcType.getName()
|
||||
+ "; did you forget to register a subtype?");
|
||||
}
|
||||
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
|
||||
|
||||
if (maintainType) {
|
||||
Streams.write(jsonObject, out);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject clone = new JsonObject();
|
||||
|
||||
if (jsonObject.has(typeFieldName)) {
|
||||
throw new JsonParseException("cannot serialize " + srcType.getName()
|
||||
+ " because it already defines a field named " + typeFieldName);
|
||||
}
|
||||
clone.add(typeFieldName, new JsonPrimitive(label));
|
||||
|
||||
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
|
||||
clone.add(e.getKey(), e.getValue());
|
||||
}
|
||||
Streams.write(clone, out);
|
||||
}
|
||||
}.nullSafe();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.internal.Streams
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adapts values whose runtime type may differ from their declaration type. This
|
||||
* is necessary when a field's type is not the same type that GSON should create
|
||||
* when deserializing that field. For example, consider these types:
|
||||
* <pre> `abstract class Shape {
|
||||
* int x;
|
||||
* int y;
|
||||
* }
|
||||
* class Circle extends Shape {
|
||||
* int radius;
|
||||
* }
|
||||
* class Rectangle extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
* class Diamond extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
* class Drawing {
|
||||
* Shape bottomShape;
|
||||
* Shape topShape;
|
||||
* }
|
||||
`</pre> *
|
||||
*
|
||||
* Without additional type information, the serialized JSON is ambiguous. Is
|
||||
* the bottom shape in this drawing a rectangle or a diamond? <pre> `{
|
||||
* "bottomShape": {
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }`</pre>
|
||||
* This class addresses this problem by adding type information to the
|
||||
* serialized JSON and honoring that type information when the JSON is
|
||||
* deserialized: <pre> `{
|
||||
* "bottomShape": {
|
||||
* "type": "Diamond",
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "type": "Circle",
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }`</pre>
|
||||
* Both the type field name (`"type"`) and the type labels (`"Rectangle"`) are configurable.
|
||||
*
|
||||
* <h3>Registering Types</h3>
|
||||
* Create a `RuntimeTypeAdapterFactory` by passing the base type and type field
|
||||
* name to the [.of] factory method. If you don't supply an explicit type
|
||||
* field name, `"type"` will be used. <pre> `RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
|
||||
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
|
||||
`</pre> *
|
||||
* Next register all of your subtypes. Every subtype must be explicitly
|
||||
* registered. This protects your application from injection attacks. If you
|
||||
* don't supply an explicit type label, the type's simple name will be used.
|
||||
* <pre> `shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
|
||||
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
|
||||
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
|
||||
`</pre> *
|
||||
* Finally, register the type adapter factory in your application's GSON builder:
|
||||
* <pre> `Gson gson = new GsonBuilder()
|
||||
* .registerTypeAdapterFactory(shapeAdapterFactory)
|
||||
* .create();
|
||||
`</pre> *
|
||||
* Like `GsonBuilder`, this API supports chaining: <pre> `RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
|
||||
* .registerSubtype(Rectangle.class)
|
||||
* .registerSubtype(Circle.class)
|
||||
* .registerSubtype(Diamond.class);
|
||||
`</pre> *
|
||||
*
|
||||
* <h3>Serialization and deserialization</h3>
|
||||
* In order to serialize and deserialize a polymorphic object,
|
||||
* you must specify the base type explicitly.
|
||||
* <pre> `Diamond diamond = new Diamond();
|
||||
* String json = gson.toJson(diamond, Shape.class);
|
||||
`</pre> *
|
||||
* And then:
|
||||
* <pre> `Shape shape = gson.fromJson(json, Shape.class);
|
||||
`</pre> *
|
||||
*/
|
||||
class RuntimeTypeAdapterFactory<T>(
|
||||
baseType: Class<*>?,
|
||||
typeFieldName: String?,
|
||||
maintainType: Boolean
|
||||
) : TypeAdapterFactory {
|
||||
|
||||
private val baseType: Class<*>
|
||||
private val typeFieldName: String
|
||||
private val labelToSubtype = mutableMapOf<String, Class<*>>()
|
||||
private val subtypeToLabel = mutableMapOf<Class<*>, String>()
|
||||
private val maintainType: Boolean
|
||||
|
||||
init {
|
||||
if (typeFieldName == null || baseType == null) {
|
||||
throw NullPointerException()
|
||||
}
|
||||
this.baseType = baseType
|
||||
this.typeFieldName = typeFieldName
|
||||
this.maintainType = maintainType
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers `type` identified by `label`. Labels are case
|
||||
* sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either `type` or `label`
|
||||
* have already been registered on this type adapter.
|
||||
*/
|
||||
fun registerSubtype(type: Class<out T>?, label: String?): RuntimeTypeAdapterFactory<T> {
|
||||
if (type == null || label == null) {
|
||||
throw NullPointerException()
|
||||
}
|
||||
require(!(subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label))) {
|
||||
"types and labels must be unique"
|
||||
}
|
||||
|
||||
labelToSubtype[label] = type
|
||||
subtypeToLabel[type] = label
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers `type` identified by its [simple][Class.getSimpleName]. Labels are case sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either `type` or its simple name
|
||||
* have already been registered on this type adapter.
|
||||
*/
|
||||
fun registerSubtype(type: Class<out T>): RuntimeTypeAdapterFactory<T> {
|
||||
return registerSubtype(type, type.simpleName)
|
||||
}
|
||||
|
||||
override fun <R : Any> create(gson: Gson, type: TypeToken<R>): TypeAdapter<R>? {
|
||||
if (type.rawType != baseType) {
|
||||
return null
|
||||
}
|
||||
|
||||
val labelToDelegate = mutableMapOf<String, TypeAdapter<*>>()
|
||||
val subtypeToDelegate = mutableMapOf<Class<*>, TypeAdapter<*>>()
|
||||
for ((key, value) in labelToSubtype) {
|
||||
val delegate = gson.getDelegateAdapter(
|
||||
this, TypeToken.get(
|
||||
value
|
||||
)
|
||||
)
|
||||
labelToDelegate[key] = delegate
|
||||
subtypeToDelegate[value] = delegate
|
||||
}
|
||||
|
||||
return object : TypeAdapter<R>() {
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): R? {
|
||||
val jsonElement = Streams.parse(reader)
|
||||
val labelJsonElement = if (maintainType) {
|
||||
jsonElement.asJsonObject[typeFieldName]
|
||||
} else {
|
||||
jsonElement.asJsonObject.remove(typeFieldName)
|
||||
}
|
||||
|
||||
if (labelJsonElement == null) {
|
||||
throw JsonParseException(
|
||||
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
|
||||
)
|
||||
}
|
||||
val label = labelJsonElement.asString
|
||||
val delegate = labelToDelegate[label] as TypeAdapter<R>?
|
||||
if (delegate == null) {
|
||||
Timber.tag("RuntimeTypeAdapter").e(
|
||||
"cannot deserialize $baseType subtype named $label; did you forget to register a subtype? $jsonElement"
|
||||
)
|
||||
return null
|
||||
}
|
||||
return delegate.fromJsonTree(jsonElement)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: R) {
|
||||
val srcType: Class<*> = value::class.java.javaClass
|
||||
val delegate =
|
||||
subtypeToDelegate[srcType] as TypeAdapter<R?>? ?: throw JsonParseException(
|
||||
"cannot serialize ${srcType.name}; did you forget to register a subtype?"
|
||||
)
|
||||
|
||||
val jsonObject = delegate.toJsonTree(value).asJsonObject
|
||||
if (maintainType) {
|
||||
Streams.write(jsonObject, out)
|
||||
return
|
||||
}
|
||||
|
||||
if (jsonObject.has(typeFieldName)) {
|
||||
throw JsonParseException(
|
||||
"cannot serialize ${srcType.name} because it already defines a field named $typeFieldName"
|
||||
)
|
||||
}
|
||||
val clone = JsonObject()
|
||||
val label = subtypeToLabel[srcType]
|
||||
clone.add(typeFieldName, JsonPrimitive(label))
|
||||
for ((key, value1) in jsonObject.entrySet()) {
|
||||
clone.add(key, value1)
|
||||
}
|
||||
Streams.write(clone, out)
|
||||
}
|
||||
}.nullSafe()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive.
|
||||
* `maintainType` flag decide if the type will be stored in pojo or not.
|
||||
*/
|
||||
fun <T> of(
|
||||
baseType: Class<T>,
|
||||
typeFieldName: String,
|
||||
maintainType: Boolean
|
||||
): RuntimeTypeAdapterFactory<T> =
|
||||
RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType)
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive.
|
||||
*/
|
||||
fun <T> of(baseType: Class<T>, typeFieldName: String): RuntimeTypeAdapterFactory<T> =
|
||||
RuntimeTypeAdapterFactory(baseType, typeFieldName, false)
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter for `baseType` using `"type"` as
|
||||
* the type field name.
|
||||
*/
|
||||
fun <T> of(baseType: Class<T>): RuntimeTypeAdapterFactory<T> =
|
||||
RuntimeTypeAdapterFactory(baseType, "type", false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class UriTypeAdapter extends TypeAdapter<Uri> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Uri value) throws IOException {
|
||||
out.value(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri read(JsonReader in) throws IOException {
|
||||
String url = in.nextString();
|
||||
return Uri.parse(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.IOException
|
||||
|
||||
class UriTypeAdapter : TypeAdapter<Uri>() {
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: Uri) {
|
||||
out.value(value.toString())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): Uri {
|
||||
return Uri.parse(reader.nextString())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class WikiSiteTypeAdapter extends TypeAdapter<WikiSite> {
|
||||
private static final String DOMAIN = "domain";
|
||||
private static final String LANGUAGE_CODE = "languageCode";
|
||||
|
||||
@Override public void write(JsonWriter out, WikiSite value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name(DOMAIN);
|
||||
out.value(value.url());
|
||||
|
||||
out.name(LANGUAGE_CODE);
|
||||
out.value(value.languageCode());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
@Override public WikiSite read(JsonReader in) throws IOException {
|
||||
// todo: legacy; remove in June 2018
|
||||
if (in.peek() == JsonToken.STRING) {
|
||||
return new WikiSite(Uri.parse(in.nextString()));
|
||||
}
|
||||
|
||||
String domain = null;
|
||||
String languageCode = null;
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String field = in.nextName();
|
||||
String val = in.nextString();
|
||||
switch (field) {
|
||||
case DOMAIN:
|
||||
domain = val;
|
||||
break;
|
||||
case LANGUAGE_CODE:
|
||||
languageCode = val;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
if (domain == null) {
|
||||
throw new JsonParseException("Missing domain");
|
||||
}
|
||||
|
||||
// todo: legacy; remove in June 2018
|
||||
if (languageCode == null) {
|
||||
return new WikiSite(domain);
|
||||
}
|
||||
return new WikiSite(domain, languageCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package fr.free.nrw.commons.wikidata.json
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||
import java.io.IOException
|
||||
|
||||
class WikiSiteTypeAdapter : TypeAdapter<WikiSite>() {
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: WikiSite) {
|
||||
out.beginObject()
|
||||
out.name(DOMAIN)
|
||||
out.value(value.url())
|
||||
|
||||
out.name(LANGUAGE_CODE)
|
||||
out.value(value.languageCode())
|
||||
out.endObject()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): WikiSite {
|
||||
// todo: legacy; remove reader June 2018
|
||||
if (reader.peek() == JsonToken.STRING) {
|
||||
return WikiSite(Uri.parse(reader.nextString()))
|
||||
}
|
||||
|
||||
var domain: String? = null
|
||||
var languageCode: String? = null
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
val field = reader.nextName()
|
||||
val value = reader.nextString()
|
||||
when (field) {
|
||||
DOMAIN -> domain = value
|
||||
LANGUAGE_CODE -> languageCode = value
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
if (domain == null) {
|
||||
throw JsonParseException("Missing domain")
|
||||
}
|
||||
|
||||
// todo: legacy; remove reader June 2018
|
||||
return if (languageCode == null) {
|
||||
WikiSite(domain)
|
||||
} else {
|
||||
WikiSite(domain, languageCode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DOMAIN = "domain"
|
||||
private const val LANGUAGE_CODE = "languageCode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.json.annotations;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
|
||||
/**
|
||||
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
|
||||
* an instantiated object.
|
||||
*
|
||||
* E.g.: @NonNull @Required private String title;
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface Required {
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package fr.free.nrw.commons.wikidata.json.annotations
|
||||
|
||||
|
||||
/**
|
||||
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
|
||||
* an instantiated object.
|
||||
*
|
||||
* E.g.: @NonNull @Required private String title;
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.FIELD)
|
||||
annotation class Required
|
||||
|
|
@ -148,7 +148,7 @@ public class Notification {
|
|||
return null;
|
||||
}
|
||||
if (primaryLink == null && primary instanceof JsonObject) {
|
||||
primaryLink = GsonUtil.getDefaultGson().fromJson(primary, Link.class);
|
||||
primaryLink = GsonUtil.INSTANCE.getDefaultGson().fromJson(primary, Link.class);
|
||||
}
|
||||
return primaryLink;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package fr.free.nrw.commons.wikidata.mwapi;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class UserInfo {
|
||||
@NonNull private String name;
|
||||
@NonNull private int id;
|
||||
|
||||
//Block information
|
||||
private int blockid;
|
||||
private String blockedby;
|
||||
private int blockedbyid;
|
||||
private String blockreason;
|
||||
private String blocktimestamp;
|
||||
private String blockexpiry;
|
||||
|
||||
// Object type is any JSON type.
|
||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
||||
@Nullable private Map<String, ?> options;
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String blockexpiry() {
|
||||
if (blockexpiry != null)
|
||||
return blockexpiry;
|
||||
else return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package fr.free.nrw.commons.wikidata.mwapi
|
||||
|
||||
data class UserInfo(
|
||||
val name: String = "",
|
||||
val id: Int = 0,
|
||||
|
||||
//Block information
|
||||
val blockid: Int = 0,
|
||||
val blockedby: String? = null,
|
||||
val blockedbyid: Int = 0,
|
||||
val blockreason: String? = null,
|
||||
val blocktimestamp: String? = null,
|
||||
val blockexpiry: String? = null,
|
||||
|
||||
// Object type is any JSON type.
|
||||
val options: Map<String, *>? = null
|
||||
) {
|
||||
fun id(): Int = id
|
||||
|
||||
fun blockexpiry(): String = blockexpiry ?: ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue