diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 3ca86ed270..1dae32f486 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import androidx.room.Entity; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.media.Depictions; import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.MediaDataExtractorUtil; import java.text.ParseException; @@ -14,7 +15,6 @@ import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.gallery.ExtMetadata; @@ -56,7 +56,7 @@ public class Media implements Parcelable { * Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories. * However unlike categories depictions is multi-lingual */ - private List> depictionList= new ArrayList<>(); + private Depictions depictions; private boolean requestedDeletion; @Nullable private LatLng coordinates; @@ -305,12 +305,10 @@ public String getCaption() { /** * @return depictions associated with the current media */ - public List> getDepiction() { - return depictionList; + public Depictions getDepiction() { + return depictions; } - - /** * Sets the file description. * @param description the new description of the file @@ -489,8 +487,8 @@ public void setCaption(String caption) { } /* Sets depictions for the current media obtained fro Wikibase API*/ - public void setDepictionList(List> depictions) { - this.depictionList = depictions; + public void setDepictions(Depictions depictions) { + this.depictions = depictions; } public void setLocalUri(@Nullable final Uri localUri) { @@ -509,8 +507,8 @@ public void setLicenseUrl(final String licenseUrl) { this.licenseUrl = licenseUrl; } - public List> getDepictionList() { - return depictionList; + public Depictions getDepictions() { + return depictions; } @Override @@ -542,7 +540,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.creator); dest.writeString(this.pageId); dest.writeStringList(this.categories); - dest.writeList(this.depictionList); + dest.writeParcelable(this.depictions, flags); dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); dest.writeParcelable(this.coordinates, flags); } @@ -568,7 +566,7 @@ protected Media(Parcel in) { final ArrayList list = new ArrayList<>(); in.readStringList(list); this.categories=list; - in.readList(depictionList,null); + in.readParcelable(Depictions.class.getClassLoader()); this.requestedDeletion = in.readByte() != 0; this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 29d5d66b56..727308f8a1 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -3,13 +3,9 @@ import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX; import androidx.core.text.HtmlCompat; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; +import fr.free.nrw.commons.media.Depictions; import fr.free.nrw.commons.media.MediaClient; import io.reactivex.Single; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import org.jetbrains.annotations.NotNull; @@ -24,10 +20,6 @@ @Singleton public class MediaDataExtractor { - private static final int LABEL_BEGIN_INDEX = 3; - private static final int LABEL_END_OFFSET = 3; - private static final int ID_BEGIN_INDEX = 1; - private static final int ID_END_OFFSET = 1; private final MediaClient mediaClient; @Inject @@ -52,10 +44,10 @@ public Single fetchMediaDetails(final String filename, final String pageI @NotNull private Media combineToMedia(final Media media, final Boolean deletionStatus, final String discussion, - final String caption, final JsonObject depiction) { + final String caption, final Depictions depictions) { media.setDiscussion(discussion); media.setCaption(caption); - media.setDepictionList(formatDepictions(depiction)); + media.setDepictions(depictions); if (deletionStatus) { media.setRequestedDeletion(true); } @@ -74,39 +66,12 @@ private Single getCaption(final String wikibaseIdentifier) { } /** - * From the Json Object extract depictions into an array list - * @param mediaResponse - * @return List containing map for depictions, the map has two keys, - * first key is for the label and second is for the url of the item - */ - private ArrayList> formatDepictions(final JsonObject mediaResponse) { - try { - final JsonArray depictionArray = (JsonArray) mediaResponse.get("Depiction"); - final ArrayList> depictedItemList = new ArrayList<>(); - for (int i = 0; i depictedObject = new HashMap<>(); - final String label = depictedItem.get("label").toString(); - final String id = depictedItem.get("id").toString(); - final String transformedLabel = label.substring(LABEL_BEGIN_INDEX, label.length()- LABEL_END_OFFSET); - final String transformedId = id.substring(ID_BEGIN_INDEX,id.length() - ID_END_OFFSET); - depictedObject.put("label", transformedLabel); //remove the additional characters obtained in label and ID object to extract the relevant string (since the string also contains extra quites that are not required) - depictedObject.put("id", transformedId); - depictedItemList.add(depictedObject); - } - return depictedItemList; - } catch (final ClassCastException | NullPointerException ignore) { - return new ArrayList<>(); - } - } - - /** - * Fetch caption and depictions from the MediaWiki API + * Fetch depictions from the MediaWiki API * @param filename the filename we will return the caption for - * @return a map containing caption and depictions (empty string in the map if no caption/depictions) + * @return Depictions */ - private Single getDepictions(final String filename) { - return mediaClient.getCaptionAndDepictions(filename) + private Single getDepictions(final String filename) { + return mediaClient.getDepictions(filename) .doOnError(throwable -> Timber.e(throwable, "error while fetching depictions")); } diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java index 20b41b7942..94311c67c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java @@ -7,6 +7,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.media.Depictions; import fr.free.nrw.commons.upload.WikidataPlace; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import java.util.Date; @@ -72,16 +73,6 @@ public static LatLng stringToLatLng(String objectList) { return readObjectFromString(objectList,LatLng.class); } - @TypeConverter - public static String listOfMapToString(List> listOfMaps) { - return writeObjectToString(listOfMaps); - } - - @TypeConverter - public static List> stringToListOfMap(String listOfMaps) { - return readObjectWithTypeToken(listOfMaps, new TypeToken>>() {}); - } - @TypeConverter public static String wikidataPlaceToString(WikidataPlace wikidataPlace) { return writeObjectToString(wikidataPlace); @@ -102,6 +93,16 @@ public static List stringToList(String depictedItems) { return readObjectWithTypeToken(depictedItems, new TypeToken>() {}); } + @TypeConverter + public static String depictionsToString(Depictions depictedItems) { + return writeObjectToString(depictedItems); + } + + @TypeConverter + public static Depictions stringToDepictions(String depictedItems) { + return readObjectFromString(depictedItems, Depictions.class); + } + private static String writeObjectToString(Object object) { return object == null ? null : getGson().toJson(object); } diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java b/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java index 21b54f0297..a64d829670 100644 --- a/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java +++ b/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java @@ -4,8 +4,8 @@ import dagger.Module; import fr.free.nrw.commons.depictions.Media.DepictedImagesContract; import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter; -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListContract; -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListPresenter; +import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract; +import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter; /** * The Dagger Module for explore:depictions related presenters and (some other objects maybe in future) diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Binding.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Binding.java deleted file mode 100644 index a165ec1a69..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Binding.java +++ /dev/null @@ -1,62 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class Binding { - - @SerializedName("subclass") - @Expose - private Subclass subclass; - @SerializedName("subclassLabel") - @Expose - private SubclassLabel subclassLabel; - @SerializedName("subclassDescription") - @Expose - private SubclassDescription subclassDescription; - /** - * No args constructor for use in serialization - * - */ - public Binding() { - } - - /** - * - * @param subclassLabel - * @param subclass - */ - public Binding(Subclass subclass, SubclassLabel subclassLabel, SubclassDescription subclassDescription) { - super(); - this.subclass = subclass; - this.subclassLabel = subclassLabel; - this.subclassDescription = subclassDescription; - } - - public Subclass getSubclass() { - return subclass; - } - - public void setSubclass(Subclass subclass) { - this.subclass = subclass; - } - - public SubclassLabel getSubclassLabel() { - return subclassLabel; - } - - public SubclassDescription getSubclassDescription(){ - return subclassDescription; - } - - public void setSubclassLabel(SubclassLabel subclassLabel) { - this.subclassLabel = subclassLabel; - } - - public void setSubclassDescription(SubclassDescription subclassDescription) { - this.subclassDescription = subclassDescription; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Head.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Head.java deleted file mode 100644 index 5ccf2ce6fe..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Head.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import java.util.List; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class Head { - - @SerializedName("vars") - @Expose - private List vars = null; - - /** - * No args constructor for use in serialization - * - */ - public Head() { - } - - /** - * - * @param vars - */ - public Head(List vars) { - super(); - this.vars = vars; - } - - public List getVars() { - return vars; - } - - public void setVars(List vars) { - this.vars = vars; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Results.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Results.java deleted file mode 100644 index 8f08be3cd1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Results.java +++ /dev/null @@ -1,37 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import java.util.List; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class Results { - - @SerializedName("bindings") - @Expose - private List bindings = null; - - /** - * No args constructor for use in serialization - */ - public Results() { - } - - /** - * @param bindings - */ - public Results(List bindings) { - super(); - this.bindings = bindings; - } - - public List getBindings() { - return bindings; - } - - public void setBindings(List bindings) { - this.bindings = bindings; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SparqlQueryResponse.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SparqlQueryResponse.java deleted file mode 100644 index 1418acff10..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SparqlQueryResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * SparqlQueryResponse obtained while fetching parent classes and sub classes for depicted items in explore - */ -public class SparqlQueryResponse { - - @SerializedName("head") - @Expose - private Head head; - @SerializedName("results") - @Expose - private Results results; - - /** - * No args constructor for use in serialization - * - */ - public SparqlQueryResponse() { - } - - /** - * - * @param results - * @param head - */ - public SparqlQueryResponse(Head head, Results results) { - super(); - this.head = head; - this.results = results; - } - - public Head getHead() { - return head; - } - - public void setHead(Head head) { - this.head = head; - } - - public Results getResults() { - return results; - } - - public void setResults(Results results) { - this.results = results; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Subclass.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Subclass.java deleted file mode 100644 index 9621883014..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/Subclass.java +++ /dev/null @@ -1,51 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class Subclass { - - @SerializedName("type") - @Expose - private String type; - @SerializedName("value") - @Expose - private String value; - - /** - * No args constructor for use in serialization - * - */ - public Subclass() { - } - - /** - * - * @param value - * @param type - */ - public Subclass(String type, String value) { - super(); - this.type = type; - this.value = value; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassDescription.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassDescription.java deleted file mode 100644 index 4a3a0efa74..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassDescription.java +++ /dev/null @@ -1,68 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class SubclassDescription { - - @SerializedName("type") - @Expose - private String type; - @SerializedName("value") - @Expose - private String value; - @SerializedName("xml:lang") - @Expose - private String xmlLang; - - /** - * No args constructor for use in serialization - * - */ - public SubclassDescription() { - } - - /** - * - * @param value - * @param xmlLang - * @param type - */ - public SubclassDescription(String type, String value, String xmlLang) { - super(); - this.type = type; - this.value = value; - this.xmlLang = xmlLang; - } - - public String getType() { - return type; - } - - /** - * returns type - */ - public void setType(String type) { - this.type = type; - } - - /** - * gets value of the depiction - */ - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - /** - * get language in which the depiction was requested - */ - public String getXmlLang() { - return xmlLang; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassLabel.java b/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassLabel.java deleted file mode 100644 index a9145e9687..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/models/SubclassLabel.java +++ /dev/null @@ -1,68 +0,0 @@ -package fr.free.nrw.commons.depictions.SubClass.models; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * Model class for parsing SparqlQueryResponse - */ -public class SubclassLabel { - - @SerializedName("type") - @Expose - private String type; - @SerializedName("value") - @Expose - private String value; - @SerializedName("xml:lang") - @Expose - private String xmlLang; - - /** - * No args constructor for use in serialization - * - */ - public SubclassLabel() { - } - - /** - * - * @param value - * @param xmlLang - * @param type - */ - public SubclassLabel(String type, String value, String xmlLang) { - super(); - this.type = type; - this.value = value; - this.xmlLang = xmlLang; - } - - public String getType() { - return type; - } - - /** - * returns type - */ - public void setType(String type) { - this.type = type; - } - - /** - * gets value of the depiction - */ - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - /** - * get language in which the depiction was requested - */ - public String getXmlLang() { - return xmlLang; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java index 86a4284060..40ea07995f 100644 --- a/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java @@ -21,7 +21,7 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment; -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment; +import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment; import fr.free.nrw.commons.explore.ViewPagerAdapter; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.theme.NavigationBaseActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListContract.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java similarity index 94% rename from app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListContract.java rename to app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java index 3de6e40881..0973149cc0 100644 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListContract.java +++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.depictions.SubClass; +package fr.free.nrw.commons.depictions.subClass; import java.io.IOException; import java.util.List; diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListFragment.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java similarity index 98% rename from app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListFragment.java rename to app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java index e65e8c76d8..5c8a3c045d 100644 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.depictions.SubClass; +package fr.free.nrw.commons.depictions.subClass; import android.content.res.Configuration; import android.os.Bundle; @@ -17,7 +17,6 @@ import com.pedrogomez.renderers.RVRendererAdapter; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Locale; diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListPresenter.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java similarity index 99% rename from app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListPresenter.java rename to app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java index a3b75151ec..1f9f4fc765 100644 --- a/app/src/main/java/fr/free/nrw/commons/depictions/SubClass/SubDepictionListPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.depictions.SubClass; +package fr.free.nrw.commons.depictions.subClass; import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt new file mode 100644 index 0000000000..da9781c06a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.depictions.subClass.models + +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem + +data class SparqlResponse(val results: Result) { + fun toDepictedItems() = + results.bindings.map { + DepictedItem( + it.itemLabel.value, + it.itemDescription?.value ?: "", + "", + false, + it.item.value.substringAfterLast("/") + ) + } +} + +data class Result(val bindings: List) + +data class Binding( + val item: SparqInfo, + val itemLabel: SparqInfo, + val itemDescription: SparqInfo? = null +) + +data class SparqInfo(val type: String, val value: String) diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index d1c0383f8a..a294ca1cf1 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -9,7 +9,7 @@ import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment; -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment; +import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment; import fr.free.nrw.commons.explore.images.SearchImageFragment; diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 09e74442d4..9f8df9de52 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -84,8 +84,7 @@ public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, toolsForgeUrl, WIKIDATA_SPARQL_QUERY_URL, BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, - BuildConfig.WIKIMEDIA_API_HOST, - gson); + gson); } @Named(NAMED_COMMONS_CSRF) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java index 3090069cc9..dcbf69a5f5 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java @@ -1,11 +1,16 @@ package fr.free.nrw.commons.explore.depictions; import androidx.annotation.Nullable; - -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - - +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.depictions.models.Search; +import fr.free.nrw.commons.media.MediaInterface; +import fr.free.nrw.commons.upload.depicts.DepictsInterface; +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; +import fr.free.nrw.commons.utils.CommonsDateUtil; +import fr.free.nrw.commons.wikidata.WikidataProperties; +import io.reactivex.Observable; +import io.reactivex.Single; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -14,19 +19,10 @@ import java.util.Date; import java.util.List; import java.util.Locale; - import javax.inject.Inject; import javax.inject.Singleton; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.depictions.models.Search; -import fr.free.nrw.commons.media.MediaInterface; -import fr.free.nrw.commons.upload.depicts.DepictsInterface; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.utils.CommonsDateUtil; -import io.reactivex.Observable; -import io.reactivex.Single; +import org.wikipedia.wikidata.DataValue.DataValueString; +import org.wikipedia.wikidata.Statement_partial; /** * Depicts Client to handle custom calls to Commons Wikibase APIs @@ -81,24 +77,19 @@ private String getThumbnailUrl(String title) { */ public Single getP18ForItem(String entityId) { return depictsInterface.getImageForEntity(entityId) - .map(commonsFilename -> { - String name; - try { - JsonObject claims = commonsFilename.getAsJsonObject("claims").getAsJsonObject(); - JsonObject p18 = claims.get("P18").getAsJsonArray().get(0).getAsJsonObject(); - JsonObject mainsnak = p18.get("mainsnak").getAsJsonObject(); - JsonObject datavalue = mainsnak.get("datavalue").getAsJsonObject(); - JsonPrimitive value = datavalue.get("value").getAsJsonPrimitive(); - name = value.toString(); - name = name.substring(1, name.length() - 1); - } catch (Exception e) { - name=""; - } - if (!name.isEmpty()){ - return getThumbnailUrl(name); - } else return NO_DEPICTED_IMAGE; - }) - .singleOrError(); + .map(claimsResponse -> { + final List imageClaim = claimsResponse.getClaims() + .get(WikidataProperties.IMAGE.getPropertyName()); + if (imageClaim != null) { + final DataValueString dataValue = (DataValueString) imageClaim + .get(0) + .getMainSnak() + .getDataValue(); + return getThumbnailUrl((dataValue.getValue())); + } + return NO_DEPICTED_IMAGE; + }) + .singleOrError(); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt b/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt new file mode 100644 index 0000000000..ae4191d069 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.media + +import android.os.Parcelable +import androidx.annotation.WorkerThread +import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS +import kotlinx.android.parcel.Parcelize +import org.wikipedia.wikidata.DataValue.DataValueEntityId +import org.wikipedia.wikidata.Entities +import java.util.* + +@Parcelize +data class Depictions(val depictions: List) : Parcelable { + companion object { + @JvmStatic + @WorkerThread + fun from(entities: Entities, mediaClient: MediaClient) = + Depictions( + entities.first?.statements + ?.getOrElse(DEPICTS.propertyName, { emptyList() }) + ?.map { statement -> + (statement.mainSnak.dataValue as DataValueEntityId).value.id + } + ?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) } + ?: emptyList() + ) + + private fun fetchLabel(mediaClient: MediaClient, id: String) = + mediaClient.getLabelForDepiction(id, Locale.getDefault().language).blockingGet() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt new file mode 100644 index 0000000000..00ca69f082 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt @@ -0,0 +1,14 @@ +package fr.free.nrw.commons.media + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import org.wikipedia.wikidata.Entities + +@Parcelize +data class IdAndLabel(val entityId: String, val entityLabel: String) : Parcelable { + constructor(entityId: String, entities: MutableMap) : this( + entityId, + entities.values.first().labels().values.first().value() + ) +} + diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java index fcbc7a22e4..ccca0f1c07 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java @@ -1,19 +1,11 @@ package fr.free.nrw.commons.media; -import android.annotation.SuppressLint; import androidx.annotation.NonNull; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.internal.LinkedTreeMap; -import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.utils.CommonsDateUtil; import io.reactivex.Observable; import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -24,6 +16,9 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.wikipedia.dataclient.mwapi.MwQueryResponse; +import org.wikipedia.wikidata.Entities; +import org.wikipedia.wikidata.Entities.Entity; +import org.wikipedia.wikidata.Entities.Label; import timber.log.Timber; /** @@ -179,9 +174,9 @@ public Single getCaptionByWikibaseIdentifier(String wikibaseIdentifier) return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier) .map(mediaDetailResponse -> { if (isSuccess(mediaDetailResponse)) { - for (CommonsWikibaseItem wikibaseItem : mediaDetailResponse.getEntities().values()) { - for (Caption caption : wikibaseItem.getLabels().values()) { - return caption.getValue(); + for (Entity wikibaseItem : mediaDetailResponse.entities().values()) { + for (Label label : wikibaseItem.labels().values()) { + return label.value(); } } } @@ -190,9 +185,8 @@ public Single getCaptionByWikibaseIdentifier(String wikibaseIdentifier) .singleOrError(); } - private boolean isSuccess(MediaDetailResponse response) { - return response != null && response.getSuccess() != null - && response.getSuccess() == 1 && response.getEntities() != null; + private boolean isSuccess(Entities response) { + return response != null && response.getSuccess() == 1 && response.entities() != null; } /** @@ -201,102 +195,29 @@ private boolean isSuccess(MediaDetailResponse response) { * @param filename * @return a map containing caption and depictions (empty string in the map if no caption/depictions) */ - public Single getCaptionAndDepictions(String filename) { - return mediaDetailInterface.fetchStructuredDataByFilename(Locale.getDefault().getLanguage(), filename) - .map(this::fetchCaptionandDepictionsFromMediaDetailResponse) + public Single getDepictions(String filename) { + return mediaDetailInterface.fetchEntitiesByFileName(Locale.getDefault().getLanguage(), filename) + .map(entities -> Depictions.from(entities, this)) .singleOrError(); } - /** - * Parses the mediaDetailResponse from API to extract captions and depictions - * @param mediaDetailResponse Response obtained from API for Media Details - * @return a map containing caption and depictions (empty string in the map if no caption/depictions) - */ - @SuppressLint("CheckResult") - private JsonObject fetchCaptionandDepictionsFromMediaDetailResponse(MediaDetailResponse mediaDetailResponse) { - JsonObject mediaDetails = new JsonObject(); - if (isSuccess(mediaDetailResponse)) { - Map entities = mediaDetailResponse.getEntities(); - try { - Map.Entry entry = entities.entrySet().iterator().next(); - CommonsWikibaseItem commonsWikibaseItem = entry.getValue(); - try { - Map labels = commonsWikibaseItem.getLabels(); - Map.Entry captionEntry = labels.entrySet().iterator().next(); - Caption caption = captionEntry.getValue(); - JsonElement jsonElement = new JsonPrimitive(caption.getValue()); - mediaDetails.add("Caption", jsonElement); - } catch (Exception e) { - JsonElement jsonElement = new JsonPrimitive(NO_CAPTION); - mediaDetails.add("Caption", jsonElement); - } - - try { - LinkedTreeMap statements = (LinkedTreeMap) commonsWikibaseItem.getStatements(); - ArrayList depictsItemList = (ArrayList) statements.get(BuildConfig.DEPICTS_PROPERTY); - JsonArray jsonArray = new JsonArray(); - for (int i = 0; i < depictsItemList.size(); i++) { - LinkedTreeMap depictedItem = depictsItemList.get(i); - LinkedTreeMap mainsnak = (LinkedTreeMap) depictedItem.get("mainsnak"); - Map datavalue = (Map) mainsnak.get("datavalue"); - LinkedTreeMap value = datavalue.get("value"); - String id = value.get("id").toString(); - JsonObject jsonObject = getLabelForDepiction(id, Locale.getDefault().getLanguage()) - .subscribeOn(Schedulers.newThread()) - .blockingGet(); - jsonArray.add(jsonObject); - } - mediaDetails.add("Depiction", jsonArray); - } catch (Exception e) { - JsonElement jsonElement = new JsonPrimitive(NO_DEPICTION); - mediaDetails.add("Depiction", jsonElement); - } - } catch (Exception e) { - JsonElement jsonElement = new JsonPrimitive(NO_CAPTION); - mediaDetails.add("Caption", jsonElement); - jsonElement = new JsonPrimitive(NO_DEPICTION); - mediaDetails.add("Depiction", jsonElement); - } - } else { - JsonElement jsonElement = new JsonPrimitive(NO_CAPTION); - mediaDetails.add("Caption", jsonElement); - jsonElement = null; - jsonElement = new JsonPrimitive(NO_DEPICTION); - mediaDetails.add("Depiction", jsonElement); - } - - return mediaDetails; - } - /** * Gets labels for Depictions using Entity Id from MediaWikiAPI * * @param entityId EntityId (Ex: Q81566) of the depict entity - * @return Json Object having label and Wikidata URL for the Depiction Entity + * @return label */ - public Single getLabelForDepiction(String entityId, String language) { - return mediaDetailInterface.getDepictions(entityId, language) - .map(jsonResponse -> { - try { - if (jsonResponse.get("success").toString().equals("1")) { - JsonObject entities = (JsonObject) jsonResponse.getAsJsonObject().get("entities"); - JsonObject responseObject = (JsonObject) entities.getAsJsonObject().get(entityId); - JsonObject labels = responseObject.getAsJsonObject("labels"); - JsonObject languageObject = labels.getAsJsonObject(language); - String label = String.valueOf(languageObject.get("value")); - - - JsonElement labelJson = new JsonPrimitive(label); - JsonElement idJson = new JsonPrimitive(entityId); - JsonObject jsonObject = new JsonObject(); - jsonObject.add("label", labelJson); - jsonObject.add("id", idJson); - return jsonObject; + public Single getLabelForDepiction(String entityId, String language) { + return mediaDetailInterface.getEntity(entityId, language) + .map(entities -> { + if (isSuccess(entities)) { + for (Entity entity : entities.entities().values()) { + for (Label label : entity.labels().values()) { + return label.value(); + } } - } catch (Exception e) { - Timber.e("Label not found"); - return new JsonObject(); - }return new JsonObject(); + } + throw new RuntimeException("failed getEntities"); }) .singleOrError(); } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index b9e5fa134e..8c2fe97552 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -58,7 +58,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.Locale; -import java.util.Map; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.wikipedia.util.DateUtil; @@ -143,7 +142,7 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean * However unlike categories depictions is multi-lingual * Ex: key: en value: monument */ - private ArrayList> depictions; + private Depictions depictions; private boolean categoriesLoaded = false; private boolean categoriesPresent = false; private boolean depictionLoaded = false; @@ -199,8 +198,6 @@ && getParentFragment() instanceof MediaDetailPagerFragment) { categoryNames = new ArrayList<>(); categoryNames.add(getString(R.string.detail_panel_cats_loading)); - depictions = new ArrayList<>(); - final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); ButterKnife.bind(this,view); @@ -330,8 +327,7 @@ private void setTextFields(Media media) { categoryNames.clear(); categoryNames.addAll(media.getCategories()); - depictions.clear(); - depictions.addAll(media.getDepiction()); + depictions=media.getDepiction(); depictionLoaded = true; @@ -344,7 +340,7 @@ private void setTextFields(Media media) { rebuildCatList(); - if(depictions != null && depictions.size() != 0) { + if(depictions != null) { rebuildDepictionList(); } else depictsLayout.setVisibility(GONE); @@ -363,11 +359,13 @@ private void setTextFields(Media media) { */ private void rebuildDepictionList() { depictionContainer.removeAllViews(); - for (int i = 0; i fetchStructuredDataByFilename(@Query("languages") String language, @Query("titles") String filename); + Observable fetchEntitiesByFileName(@Query("languages") String language, @Query("titles") String filename); /** * Gets labels for Depictions using Entity Id from MediaWikiAPI @@ -29,7 +25,7 @@ public interface MediaDetailInterface { * @param language user's locale */ @GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1") - Observable getDepictions(@Query("ids") String entityId, @Query("languages") String language); + Observable getEntity(@Query("ids") String entityId, @Query("languages") String language); /** * Fetches caption using wikibaseIdentifier @@ -37,5 +33,5 @@ public interface MediaDetailInterface { * @param wikibaseIdentifier pageId for the media */ @GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki") - Observable getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier); + Observable getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier); } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailResponse.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailResponse.java deleted file mode 100644 index df9536c4c1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.free.nrw.commons.media; - -import com.google.gson.annotations.SerializedName; - -import java.util.Map; - -/** - * Model class for object while fetching structured data - */ -public class MediaDetailResponse { - - @SerializedName("entities") - private Map entities; - @SerializedName("success") - private Integer success; - - /** - * No args constructor for use in serialization - */ - public MediaDetailResponse() { - } - - /** - * @param success - * @param entities - */ - public MediaDetailResponse(Map entities, Integer success) { - super(); - this.entities = entities; - this.success = success; - } - - public Map getEntities() { - return entities; - } - - - public Integer getSuccess() { - return success; - } - - public void setSuccess(Integer success) { - this.success = success; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index c60349175c..c3fcf9b089 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -6,9 +6,7 @@ import fr.free.nrw.commons.achievements.FeaturedImages; import fr.free.nrw.commons.achievements.FeedbackResponse; import fr.free.nrw.commons.campaigns.CampaignResponseDTO; -import fr.free.nrw.commons.depictions.SubClass.models.Binding; -import fr.free.nrw.commons.depictions.SubClass.models.SparqlQueryResponse; -import fr.free.nrw.commons.depictions.SubClass.models.SubclassDescription; +import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.model.NearbyResponse; @@ -30,8 +28,7 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.json.JSONArray; -import org.json.JSONObject; +import org.jetbrains.annotations.NotNull; import timber.log.Timber; /** @@ -39,136 +36,134 @@ */ @Singleton public class OkHttpJsonApiClient { - private static final String THUMB_SIZE = "640"; - private final OkHttpClient okHttpClient; - private final HttpUrl wikiMediaToolforgeUrl; - private final String sparqlQueryUrl; - private final String campaignsUrl; - private final String commonsBaseUrl; - private Gson gson; + private final OkHttpClient okHttpClient; + private final HttpUrl wikiMediaToolforgeUrl; + private final String sparqlQueryUrl; + private final String campaignsUrl; + private final Gson gson; - @Inject - public OkHttpJsonApiClient(OkHttpClient okHttpClient, - HttpUrl wikiMediaToolforgeUrl, - String sparqlQueryUrl, - String campaignsUrl, - String commonsBaseUrl, - Gson gson) { - this.okHttpClient = okHttpClient; - this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; - this.sparqlQueryUrl = sparqlQueryUrl; - this.campaignsUrl = campaignsUrl; - this.commonsBaseUrl = commonsBaseUrl; - this.gson = gson; + @Inject + public OkHttpJsonApiClient(OkHttpClient okHttpClient, + HttpUrl wikiMediaToolforgeUrl, + String sparqlQueryUrl, + String campaignsUrl, + Gson gson) { + this.okHttpClient = okHttpClient; + this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; + this.sparqlQueryUrl = sparqlQueryUrl; + this.campaignsUrl = campaignsUrl; + this.gson = gson; + } + + @NonNull + public Single getUploadCount(String userName) { + HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); + urlBuilder + .addPathSegments("uploadsbyuser.py") + .addQueryParameter("user", userName); + + if (ConfigUtils.isBetaFlavour()) { + urlBuilder.addQueryParameter("labs", "commonswiki"); } - @NonNull - public Single getUploadCount(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("uploadsbyuser.py") - .addQueryParameter("user", userName); + Request request = new Request.Builder() + .url(urlBuilder.build()) + .build(); - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); + return Single.fromCallable(() -> { + Response response = okHttpClient.newCall(request).execute(); + if (response != null && response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (null != responseBody) { + String responseBodyString = responseBody.string().trim(); + if (!TextUtils.isEmpty(responseBodyString)) { + try { + return Integer.parseInt(responseBodyString); + } catch (NumberFormatException e) { + Timber.e(e); + } + } } + } + return 0; + }); + } - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); + @NonNull + public Single getWikidataEdits(String userName) { + HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); + urlBuilder + .addPathSegments("wikidataedits.py") + .addQueryParameter("user", userName); - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.isSuccessful()) { - ResponseBody responseBody = response.body(); - if (null != responseBody) { - String responseBodyString = responseBody.string().trim(); - if (!TextUtils.isEmpty(responseBodyString)) { - try { - return Integer.parseInt(responseBodyString); - } catch (NumberFormatException e) { - Timber.e(e); - } - } - } - } - return 0; - }); + if (ConfigUtils.isBetaFlavour()) { + urlBuilder.addQueryParameter("labs", "commonswiki"); } - @NonNull - public Single getWikidataEdits(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("wikidataedits.py") - .addQueryParameter("user", userName); + Request request = new Request.Builder() + .url(urlBuilder.build()) + .build(); - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); + return Single.fromCallable(() -> { + Response response = okHttpClient.newCall(request).execute(); + if (response != null && + response.isSuccessful() && response.body() != null) { + String json = response.body().string(); + if (json == null) { + return 0; } + GetWikidataEditCountResponse countResponse = gson + .fromJson(json, GetWikidataEditCountResponse.class); + if (null != countResponse) { + return countResponse.getWikidataEditCount(); + } + } + return 0; + }); + } - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && - response.isSuccessful() && response.body() != null) { - String json = response.body().string(); - if (json == null) { - return 0; - } - GetWikidataEditCountResponse countResponse = gson.fromJson(json, GetWikidataEditCountResponse.class); - if (null != countResponse) { - return countResponse.getWikidataEditCount(); - } - } - return 0; - }); - } - - /** - * This takes userName as input, which is then used to fetch the feedback/achievements - * statistics using OkHttp and JavaRx. This function return JSONObject - * - * @param userName MediaWiki user name - * @return - */ - public Single getAchievements(String userName) { - final String fetchAchievementUrlTemplate = - wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki" : "/feedback.py"); - return Single.fromCallable(() -> { - String url = String.format( - Locale.ENGLISH, - fetchAchievementUrlTemplate, - userName); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", userName); - Timber.i("Url %s", urlBuilder.toString()); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - Timber.d("Response for achievements is %s", json); - try { - return gson.fromJson(json, FeedbackResponse.class); - } catch (Exception e) { - return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, ""); - } + /** + * This takes userName as input, which is then used to fetch the feedback/achievements statistics + * using OkHttp and JavaRx. This function return JSONObject + * + * @param userName MediaWiki user name + * @return + */ + public Single getAchievements(String userName) { + final String fetchAchievementUrlTemplate = + wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki" + : "/feedback.py"); + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchAchievementUrlTemplate, + userName); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + Timber.i("Url %s", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + Response response = okHttpClient.newCall(request).execute(); + if (response != null && response.body() != null && response.isSuccessful()) { + String json = response.body().string(); + if (json == null) { + return null; + } + Timber.d("Response for achievements is %s", json); + try { + return gson.fromJson(json, FeedbackResponse.class); + } catch (Exception e) { + return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, ""); + } - } - return null; - }); - } + } + return null; + }); + } public Observable> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException { String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); @@ -178,148 +173,90 @@ public Observable> getNearbyPlaces(LatLng cur, String language, doub .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude())) .replace("${LANG}", language); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); + HttpUrl.Builder urlBuilder = HttpUrl + .parse(sparqlQueryUrl) + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json"); - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); + Request request = new Request.Builder() + .url(urlBuilder.build()) + .build(); - return Observable.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return new ArrayList<>(); - } - NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - List bindings = nearbyResponse.getResults().getBindings(); - List places = new ArrayList<>(); - for (NearbyResultItem item : bindings) { - places.add(Place.from(item)); - } - return places; - } - return new ArrayList<>(); - }); - } + return Observable.fromCallable(() -> { + Response response = okHttpClient.newCall(request).execute(); + if (response != null && response.body() != null && response.isSuccessful()) { + String json = response.body().string(); + if (json == null) { + return new ArrayList<>(); + } + NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); + List bindings = nearbyResponse.getResults().getBindings(); + List places = new ArrayList<>(); + for (NearbyResultItem item : bindings) { + places.add(Place.from(item)); + } + return places; + } + return new ArrayList<>(); + }); + } - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. - * Example: bridge -> suspended bridge, aqueduct, etc - */ - public Observable> getChildQIDs(String qid) throws IOException { - String queryString = FileUtils.readFromResource("/queries/subclasses_query.rq"); - String query = queryString. - replace("${QID}", qid) - .replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\""); - Timber.e(query); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - return Observable.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - String json = response.body().string(); - SparqlQueryResponse example = gson.fromJson(json, SparqlQueryResponse.class); - List bindings = example.getResults().getBindings(); - ArrayList subItems = new ArrayList<>(); - for (Binding binding : bindings) { - if (binding.getSubclassLabel().getXmlLang() != null) { - String label = binding.getSubclassLabel().getValue(); - String entityId = binding.getSubclass().getValue(); - entityId = entityId.substring(entityId.lastIndexOf("/") + 1); - String description = ""; - SubclassDescription subclassDescription = binding.getSubclassDescription(); - if (subclassDescription != null - && subclassDescription.getXmlLang() != null) { - description = subclassDescription.getValue(); - } - subItems.add(new DepictedItem(label, description, "", false, entityId)); - Timber.e(label); - } - } - return subItems; - }).doOnError(throwable -> { - Timber.e(throwable.toString()); - }); - } + /** + * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: + * bridge -> suspended bridge, aqueduct, etc + */ + public Observable> getChildQIDs(String qid) throws IOException { + return depictedItemsFrom(sparqlQuery(qid, "/queries/subclasses_query.rq")); + } - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. - * Example: bridge -> suspended bridge, aqueduct, etc - */ - public Observable> getParentQIDs(String qid) throws IOException { - String queryString = FileUtils.readFromResource("/queries/parentclasses_query.rq"); - String query = queryString. - replace("${QID}", qid) - .replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\""); - Timber.e(query); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - return Observable.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - try { - String json = response.body().string(); - JSONObject jsonObject = new JSONObject(json); - ArrayList subItems = new ArrayList<>(); - JSONObject results = (JSONObject) jsonObject.get("results"); - JSONArray bindings = (JSONArray) results.get("bindings"); - for (int i = 0; i < bindings.length(); i++) { - Timber.e(bindings.get(i).getClass().toString()); - JSONObject object = (JSONObject) bindings.get(i); - JSONObject parentClassLabel = (JSONObject) object.get("parentClassLabel"); - if (parentClassLabel.get("value") != null) { - String labelString = parentClassLabel.getString("value"); - JSONObject parentClass = (JSONObject) object.get("parentClass"); - if (parentClass.get("value") != null) { - String entityId = parentClass.getString("value"); - entityId = entityId.substring(entityId.lastIndexOf("/") + 1); - String description = ""; - if (!object.isNull("parentClassDescription")) { - JSONObject parentClassDescription = (JSONObject) object - .get("parentClassDescription"); - description = parentClassDescription.getString("value"); - } - subItems.add(new DepictedItem(labelString, description, "", false, entityId)); - } - } - } - return subItems; - } catch (Exception e) { - return new ArrayList(); - } - }).doOnError(throwable -> { - Timber.e("line578"+throwable.toString()); - }); - } + /** + * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: + * bridge -> suspended bridge, aqueduct, etc + */ + public Observable> getParentQIDs(String qid) throws IOException { + return depictedItemsFrom(sparqlQuery(qid, "/queries/parentclasses_query.rq")); + } - public Single getCampaigns() { - return Single.fromCallable(() -> { - Request request = new Request.Builder().url(campaignsUrl) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - return gson.fromJson(json, CampaignResponseDTO.class); - } - return null; - }); - } + private Observable> depictedItemsFrom(Request request) { + return Observable.fromCallable(() -> { + try (ResponseBody body = okHttpClient.newCall(request).execute().body()) { + return gson.fromJson(body.string(), SparqlResponse.class).toDepictedItems(); + }catch (Exception e) { + Timber.e(e); + return new ArrayList(); + } + }).doOnError(Timber::e); + } + + @NotNull + private Request sparqlQuery(String qid, String fileName) throws IOException { + String query = FileUtils.readFromResource(fileName). + replace("${QID}", qid) + .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\""); + HttpUrl.Builder urlBuilder = HttpUrl + .parse(sparqlQueryUrl) + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json"); + return new Request.Builder() + .url(urlBuilder.build()) + .build(); + } + + public Single getCampaigns() { + return Single.fromCallable(() -> { + Request request = new Request.Builder().url(campaignsUrl) + .build(); + Response response = okHttpClient.newCall(request).execute(); + if (response != null && response.body() != null && response.isSuccessful()) { + String json = response.body().string(); + if (json == null) { + return null; + } + return gson.fromJson(json, CampaignResponseDTO.class); + } + return null; + }); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsInterface.java index 846d864d86..e6df617b2a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsInterface.java @@ -1,10 +1,8 @@ package fr.free.nrw.commons.upload.depicts; -import com.google.gson.JsonObject; - -import fr.free.nrw.commons.depictions.models.DepictionResponse; import fr.free.nrw.commons.wikidata.model.DepictSearchResponse; import io.reactivex.Observable; +import org.wikipedia.wikidata.ClaimsResponse; import retrofit2.http.GET; import retrofit2.http.Query; @@ -26,5 +24,5 @@ public interface DepictsInterface { Observable searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset); @GET("/w/api.php?action=wbgetclaims&format=json&property=P18") - Observable getImageForEntity(@Query("entity") String entityId); + Observable getImageForEntity(@Query("entity") String entityId); } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index fc86998513..7fedfc49ec 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -5,8 +5,7 @@ import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.Nullable; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; +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; @@ -24,8 +23,8 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; -import org.jetbrains.annotations.NotNull; import org.wikipedia.dataclient.mwapi.MwPostResponse; +import org.wikipedia.wikidata.EditClaim; import timber.log.Timber; /** @@ -42,20 +41,22 @@ public class WikidataEditService { private final Context context; private final WikidataEditListener wikidataEditListener; private final JsonKvStore directKvStore; - private final WikiBaseClient wikiBaseClient; + 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 WikidataClient wikidataClient, final Gson gson) { this.context = context; this.wikidataEditListener = wikidataEditListener; this.directKvStore = directKvStore; this.wikiBaseClient = wikiBaseClient; this.wikidataClient = wikidataClient; + this.gson = gson; } /** @@ -65,10 +66,13 @@ public WikidataEditService(final Context context, @SuppressLint("CheckResult") private Observable addDepictsProperty(final String fileEntityId, final WikidataItem depictedItem) { - // Wikipedia:Sandbox (Q10) - final String data = depictionJson(ConfigUtils.isBetaFlavour() ? "Q10" : depictedItem.getId()); - return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, data) + final EditClaim data = editClaim( + ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10) + : depictedItem.getId() + ); + + return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) .doOnNext(success -> { if (success) { Timber.d("DEPICTS property was set successfully for %s", fileEntityId); @@ -83,34 +87,8 @@ private Observable addDepictsProperty(final String fileEntityId, .subscribeOn(Schedulers.io()); } - @NotNull - private String depictionJson(final String entityId) { - final JsonObject value = new JsonObject(); - value.addProperty("entity-type", "item"); - value.addProperty("numeric-id", entityId.replace("Q", "")); - value.addProperty("id", entityId); - - final JsonObject dataValue = new JsonObject(); - dataValue.add("value", value); - dataValue.addProperty("type", "wikibase-entityid"); - - final JsonObject mainSnak = new JsonObject(); - mainSnak.addProperty("snaktype", "value"); - mainSnak.addProperty("property", WikidataProperties.DEPICTS.getPropertyName()); - mainSnak.add("datavalue", dataValue); - - final JsonObject claim = new JsonObject(); - claim.add("mainsnak", mainSnak); - claim.addProperty("type", "statement"); - claim.addProperty("rank", "preferred"); - - final JsonArray claims = new JsonArray(); - claims.add(claim); - - final JsonObject jsonData = new JsonObject(); - jsonData.add("claims", claims); - - return jsonData.toString(); + private EditClaim editClaim(final String entityId) { + return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName()); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.java deleted file mode 100644 index de584dd53d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -import fr.free.nrw.commons.BuildConfig; - -enum WikidataProperties { - IMAGE("P18"), - DEPICTS(BuildConfig.DEPICTS_PROPERTY); - - private final String propertyName; - - WikidataProperties(final String propertyName) { - this.propertyName = propertyName; - } - - public String getPropertyName() { - return propertyName; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt new file mode 100644 index 0000000000..30a11f92c9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.wikidata + +import fr.free.nrw.commons.BuildConfig + +enum class WikidataProperties(val propertyName: String) { + IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY); + +} diff --git a/app/src/main/resources/queries/parentclasses_query.rq b/app/src/main/resources/queries/parentclasses_query.rq index ee37615055..0418b5965e 100644 --- a/app/src/main/resources/queries/parentclasses_query.rq +++ b/app/src/main/resources/queries/parentclasses_query.rq @@ -1,4 +1,4 @@ -SELECT ?parentClass ?parentClassLabel ?parentClassDescription WHERE { - wd:${QID} wdt:P279 ?parentClass. +SELECT ?item ?itemLabel ?itemDescription WHERE { + wd:${QID} wdt:P279 ?item. SERVICE wikibase:label { bd:serviceParam wikibase:language ${LANG}. } } diff --git a/app/src/main/resources/queries/subclasses_query.rq b/app/src/main/resources/queries/subclasses_query.rq index 6bd9dbcbeb..fd4d18b39b 100644 --- a/app/src/main/resources/queries/subclasses_query.rq +++ b/app/src/main/resources/queries/subclasses_query.rq @@ -1,4 +1,4 @@ -SELECT ?subclass ?subclassLabel ?subclassDescription WHERE { - ?subclass wdt:P279 wd:${QID}. +SELECT ?item ?itemLabel ?itemDescription WHERE { + ?item wdt:P279 wd:${QID}. SERVICE wikibase:label { bd:serviceParam wikibase:language ${LANG}. } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/depictions/SubDepictionListPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/depictions/SubDepictionListPresenterTest.kt index 29d0fa89e1..803ac24714 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/depictions/SubDepictionListPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/depictions/SubDepictionListPresenterTest.kt @@ -1,8 +1,7 @@ package fr.free.nrw.commons.depictions -import org.mockito.Mockito.verify -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListContract -import fr.free.nrw.commons.depictions.SubClass.SubDepictionListPresenter +import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract +import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter import fr.free.nrw.commons.explore.depictions.DepictsClient import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient @@ -15,6 +14,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations class SubDepictionListPresenterTest { @@ -34,7 +34,7 @@ class SubDepictionListPresenterTest { @Mock internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null - var testObservable: Observable>? = null + var testObservable: Observable>? = null @Mock lateinit var depictedItem: DepictedItem @@ -76,4 +76,4 @@ class SubDepictionListPresenterTest { testScheduler?.triggerActions() verify(view)?.onSuccess(depictedItems) } -} \ No newline at end of file +} diff --git a/data-client/src/main/java/org/wikipedia/json/GsonUtil.java b/data-client/src/main/java/org/wikipedia/json/GsonUtil.java index 81d1675d6e..6984af00a4 100644 --- a/data-client/src/main/java/org/wikipedia/json/GsonUtil.java +++ b/data-client/src/main/java/org/wikipedia/json/GsonUtil.java @@ -1,21 +1,20 @@ package org.wikipedia.json; import android.net.Uri; - import androidx.annotation.VisibleForTesting; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; - import org.wikipedia.dataclient.SharedPreferenceCookieManager; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.page.Namespace; +import org.wikipedia.wikidata.DataValue; 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()) diff --git a/data-client/src/main/java/org/wikipedia/json/RuntimeTypeAdapterFactory.java b/data-client/src/main/java/org/wikipedia/json/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000000..f0acaf72d6 --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/json/RuntimeTypeAdapterFactory.java @@ -0,0 +1,280 @@ +package org.wikipedia.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: + *
   {@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;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * 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.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * 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. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, 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 RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory(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 RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(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 registerSubtype(Class 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 registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> 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() { + @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 delegate = (TypeAdapter) 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 delegate = (TypeAdapter) 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 e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} diff --git a/data-client/src/main/java/org/wikipedia/wikidata/ClaimsResponse.kt b/data-client/src/main/java/org/wikipedia/wikidata/ClaimsResponse.kt new file mode 100644 index 0000000000..50f7725c1a --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/ClaimsResponse.kt @@ -0,0 +1,4 @@ +package org.wikipedia.wikidata + + +data class ClaimsResponse(val claims: Map>) diff --git a/data-client/src/main/java/org/wikipedia/wikidata/DataValue.kt b/data-client/src/main/java/org/wikipedia/wikidata/DataValue.kt new file mode 100644 index 0000000000..e7e06876b5 --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/DataValue.kt @@ -0,0 +1,84 @@ +package org.wikipedia.wikidata + +import org.wikipedia.json.RuntimeTypeAdapterFactory + +/*"datavalue": { + "value": { + "entity-type": "item", + "id": "Q30", + "numeric-id": 30 + }, + "type": "wikibase-entityid" + } + OR + "datavalue": { + "value": "SomePicture.jpg", + "type": "string" + } + OR + "datavalue": { + "value": { + "latitude": 37.7733, + "longitude": -122.412255, + "altitude": null, + "precision": 1.0e-6, + "globe": "http://www.wikidata.org/entity/Q2" + }, + "type": "globecoordinate" + } + OR + "datavalue": { + "value": { + "time": "+2019-12-03T00:00:00Z", + "timezone": 0, + "before": 0, + "after": 0, + "precision": 11, + "calendarmodel": "http://www.wikidata.org/entity/Q1985727" + }, + "type": "time" + } + */ +sealed class DataValue(val type: String) { + companion object { + @JvmStatic + val polymorphicTypeAdapter = + RuntimeTypeAdapterFactory.of(DataValue::class.java, DataValue::type.name) + .registerSubtype(DataValueEntityId::class.java, DataValueEntityId.TYPE) + .registerSubtype(DataValueString::class.java, DataValueString.TYPE) + .registerSubtype( + DataValueGlobeCoordinate_partial::class.java, + DataValueGlobeCoordinate_partial.TYPE + ) + .registerSubtype( + DataValueTime_partial::class.java, + DataValueTime_partial.TYPE + ) + } + + data class DataValueEntityId(val value: WikiBaseEntityValue) : + DataValue(TYPE) { + companion object { + const val TYPE = "wikibase-entityid" + } + } + + data class DataValueString(val value: String) : DataValue(TYPE) { + companion object { + const val TYPE = "string" + } + } + + class DataValueGlobeCoordinate_partial() : + DataValue(TYPE) { + companion object { + const val TYPE = "globecoordinate" + } + } + + class DataValueTime_partial() : DataValue(TYPE) { + companion object { + const val TYPE = "time" + } + } +} diff --git a/data-client/src/main/java/org/wikipedia/wikidata/EditClaim.kt b/data-client/src/main/java/org/wikipedia/wikidata/EditClaim.kt new file mode 100644 index 0000000000..304047b303 --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/EditClaim.kt @@ -0,0 +1,31 @@ +package org.wikipedia.wikidata + +import org.wikipedia.wikidata.DataValue.DataValueEntityId + + +data class EditClaim(val claims: List) { + + companion object { + @JvmStatic + fun from(entityId: String, propertyName: String) = + EditClaim( + listOf( + Statement_partial( + Snak_partial( + "value", + propertyName, + DataValueEntityId( + WikiBaseEntityValue( + "item", + entityId, + entityId.removePrefix("Q").toLong() + ) + ) + ), + "statement", + "preferred" + ) + ) + ) + } +} diff --git a/data-client/src/main/java/org/wikipedia/wikidata/Entities.java b/data-client/src/main/java/org/wikipedia/wikidata/Entities.java index 4b346febf4..6939f56fa5 100644 --- a/data-client/src/main/java/org/wikipedia/wikidata/Entities.java +++ b/data-client/src/main/java/org/wikipedia/wikidata/Entities.java @@ -2,14 +2,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import java.util.Collections; +import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwResponse; import org.wikipedia.json.PostProcessingTypeAdapter; -import java.util.Collections; -import java.util.Map; - @SuppressWarnings("unused") public class Entities extends MwResponse implements PostProcessingTypeAdapter.PostProcessable { @Nullable private Map entities; @@ -19,6 +18,10 @@ public class Entities extends MwResponse implements PostProcessingTypeAdapter.Po return entities; } + public int getSuccess() { + return success; + } + @Nullable public Entity getFirst() { if (entities == null) { return null; @@ -39,6 +42,7 @@ public static class Entity { @Nullable private Map labels; @Nullable private Map descriptions; @Nullable private Map sitelinks; + @Nullable private Map> statements; @Nullable private String missing; @NonNull public String id() { @@ -57,6 +61,11 @@ public static class Entity { return sitelinks != null ? sitelinks : Collections.emptyMap(); } + @Nullable + public Map> getStatements() { + return statements; + } + boolean isMissing() { return "-1".equals(id) && missing != null; } diff --git a/data-client/src/main/java/org/wikipedia/wikidata/Snak_partial.kt b/data-client/src/main/java/org/wikipedia/wikidata/Snak_partial.kt new file mode 100644 index 0000000000..db46615646 --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/Snak_partial.kt @@ -0,0 +1,22 @@ +package org.wikipedia.wikidata + +import com.google.gson.annotations.SerializedName + +/*"mainsnak": { + "snaktype": "value", + "property": "P17", + "datatype": "wikibase-item", + "datavalue": { + "value": { + "entity-type": "item", + "id": "Q30", + "numeric-id": 30 + }, + "type": "wikibase-entityid" + } +}*/ +data class Snak_partial( + @SerializedName("snaktype") val snakType: String, + val property: String, + @SerializedName("datavalue") val dataValue: DataValue +) diff --git a/data-client/src/main/java/org/wikipedia/wikidata/Statement_partial.kt b/data-client/src/main/java/org/wikipedia/wikidata/Statement_partial.kt new file mode 100644 index 0000000000..7258e354e2 --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/Statement_partial.kt @@ -0,0 +1,25 @@ +package org.wikipedia.wikidata + +import com.google.gson.annotations.SerializedName + +/*{ + "id": "q60$5083E43C-228B-4E3E-B82A-4CB20A22A3FB", + "mainsnak": {}, + "type": "statement", + "rank": "normal", + "qualifiers": { + "P580": [], + "P5436": [] +} + "references": [ + { + "hash": "d103e3541cc531fa54adcaffebde6bef28d87d32", + "snaks": [] + } + ] +}*/ +data class Statement_partial( + @SerializedName("mainsnak") val mainSnak: Snak_partial, + val type: String, + val rank: String +) diff --git a/data-client/src/main/java/org/wikipedia/wikidata/WikiBaseEntityValue.kt b/data-client/src/main/java/org/wikipedia/wikidata/WikiBaseEntityValue.kt new file mode 100644 index 0000000000..26d27fe5fa --- /dev/null +++ b/data-client/src/main/java/org/wikipedia/wikidata/WikiBaseEntityValue.kt @@ -0,0 +1,14 @@ +package org.wikipedia.wikidata + +import com.google.gson.annotations.SerializedName + +/*"value": { + "entity-type": "item", + "id": "Q30", + "numeric-id": 30 +}*/ +data class WikiBaseEntityValue( + @SerializedName("entity-type") val entityType: String, + val id: String, + val numericId: Long +)