Convert wikidata/mwapi to kotlin (part 3) (#6004)

* Convert Edit to kotlin along with deleting unused class

* Converted ExtMetadata to kotlin

* Convert ImageInfo to kotlin

* Removed unused class

* Convert Notification to kotlin

* Convert PageProperties to kotlin

* Convert PageTitle to kotlin

* Convert Namespace to kotlin
This commit is contained in:
Paul Hawke 2024-12-06 21:20:06 -06:00 committed by GitHub
parent 64354fb9e4
commit 015c5d5c63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 832 additions and 1133 deletions

View file

@ -11,7 +11,6 @@ import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.gallery.ExtMetadata
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import org.apache.commons.lang3.StringUtils
import java.text.ParseException
import java.util.Date
import javax.inject.Inject
@ -24,7 +23,7 @@ class MediaConverter
entity: Entities.Entity,
imageInfo: ImageInfo,
): Media {
val metadata = imageInfo.metadata
val metadata = imageInfo.getMetadata()
requireNotNull(metadata) { "No metadata" }
// Stores mapping of title attribute to hidden attribute of each category
val myMap = mutableMapOf<String, Boolean>()
@ -32,8 +31,8 @@ class MediaConverter
return Media(
page.pageId().toString(),
imageInfo.thumbUrl.takeIf { it.isNotBlank() } ?: imageInfo.originalUrl,
imageInfo.originalUrl,
imageInfo.getThumbUrl().takeIf { it.isNotBlank() } ?: imageInfo.getOriginalUrl(),
imageInfo.getOriginalUrl(),
page.title(),
metadata.imageDescription(),
safeParseDate(metadata.dateTime()),
@ -41,7 +40,7 @@ class MediaConverter
metadata.prefixedLicenseUrl,
getAuthor(metadata),
getAuthor(metadata),
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories),
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()),
metadata.latLng,
entity.labels().mapValues { it.value.value() },
entity.descriptions().mapValues { it.value.value() },
@ -104,9 +103,5 @@ private val ExtMetadata.prefixedLicenseUrl: String
}
private val ExtMetadata.latLng: LatLng?
get() =
if (!StringUtils.isBlank(gpsLatitude) && !StringUtils.isBlank(gpsLongitude)) {
LatLng(gpsLatitude.toDouble(), gpsLongitude.toDouble(), 0.0f)
} else {
null
}
get() = LatLng.latLongOrNull(gpsLatitude(), gpsLongitude())

View file

@ -41,6 +41,13 @@ data class LatLng(
* Accepts a non-null [Location] and converts it to a [LatLng].
*/
companion object {
fun latLongOrNull(latitude: String?, longitude: String?): LatLng? =
if (!latitude.isNullOrBlank() && !longitude.isNullOrBlank()) {
LatLng(latitude.toDouble(), longitude.toDouble(), 0.0f)
} else {
null
}
/**
* gets the latitude and longitude of a given non-null location
* @param location the non-null location of the user

View file

@ -64,8 +64,8 @@ class NotificationClient
return Notification(
notificationType = notificationType,
notificationText = notificationText,
date = DateUtil.getMonthOnlyDateString(timestamp),
link = contents?.links?.primary?.url ?: "",
date = DateUtil.getMonthOnlyDateString(getTimestamp()),
link = contents?.links?.getPrimary()?.url ?: "",
iconUrl = "",
notificationId = id().toString(),
)

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.wikidata.model.edit;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
public class Edit extends MwPostResponse {
@Nullable private Result edit;
@Nullable public Result edit() {
return edit;
}
public class Result {
@Nullable private String result;
@Nullable private String code;
@Nullable private String info;
@Nullable private String warning;
public boolean editSucceeded() {
return "Success".equals(result);
}
@Nullable public String code() {
return code;
}
@Nullable public String info() {
return info;
}
@Nullable public String warning() {
return warning;
}
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.wikidata.model.edit
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse
class Edit : MwPostResponse() {
private val edit: Result? = null
fun edit(): Result? = edit
class Result {
private val result: String? = null
private val code: String? = null
private val info: String? = null
private val warning: String? = null
fun editSucceeded(): Boolean =
"Success" == result
fun code(): String? = code
fun info(): String? = info
fun warning(): String? = warning
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.wikidata.model.edit;
import android.os.Parcel;
import android.os.Parcelable;
import fr.free.nrw.commons.wikidata.model.BaseModel;
public abstract class EditResult extends BaseModel implements Parcelable {
private final String result;
public EditResult(String result) {
this.result = result;
}
protected EditResult(Parcel in) {
this.result = in.readString();
}
public String getResult() {
return result;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(result);
}
}

View file

@ -1,102 +0,0 @@
package fr.free.nrw.commons.wikidata.model.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
public class ExtMetadata {
@SerializedName("DateTime") @Nullable private Values dateTime;
@SerializedName("ObjectName") @Nullable private Values objectName;
@SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension;
@SerializedName("Categories") @Nullable private Values categories;
@SerializedName("Assessments") @Nullable private Values assessments;
@SerializedName("GPSLatitude") @Nullable private Values gpsLatitude;
@SerializedName("GPSLongitude") @Nullable private Values gpsLongitude;
@SerializedName("ImageDescription") @Nullable private Values imageDescription;
@SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal;
@SerializedName("Artist") @Nullable private Values artist;
@SerializedName("Credit") @Nullable private Values credit;
@SerializedName("Permission") @Nullable private Values permission;
@SerializedName("AuthorCount") @Nullable private Values authorCount;
@SerializedName("LicenseShortName") @Nullable private Values licenseShortName;
@SerializedName("UsageTerms") @Nullable private Values usageTerms;
@SerializedName("LicenseUrl") @Nullable private Values licenseUrl;
@SerializedName("AttributionRequired") @Nullable private Values attributionRequired;
@SerializedName("Copyrighted") @Nullable private Values copyrighted;
@SerializedName("Restrictions") @Nullable private Values restrictions;
@SerializedName("License") @Nullable private Values license;
@NonNull public String licenseShortName() {
return StringUtils.defaultString(licenseShortName == null ? null : licenseShortName.value());
}
@NonNull public String licenseUrl() {
return StringUtils.defaultString(licenseUrl == null ? null : licenseUrl.value());
}
@NonNull public String license() {
return StringUtils.defaultString(license == null ? null : license.value());
}
@NonNull public String imageDescription() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.value());
}
@NonNull public String imageDescriptionSource() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.source());
}
@NonNull public String objectName() {
return StringUtils.defaultString(objectName == null ? null : objectName.value());
}
@NonNull public String usageTerms() {
return StringUtils.defaultString(usageTerms == null ? null : usageTerms.value());
}
@NonNull public String dateTimeOriginal() {
return StringUtils.defaultString(dateTimeOriginal == null ? null : dateTimeOriginal.value());
}
@NonNull public String dateTime() {
return StringUtils.defaultString(dateTime == null ? null : dateTime.value());
}
@NonNull public String artist() {
return StringUtils.defaultString(artist == null ? null : artist.value());
}
@NonNull public String getCategories() {
return StringUtils.defaultString(categories == null ? null : categories.value());
}
@NonNull public String getGpsLatitude() {
return StringUtils.defaultString(gpsLatitude == null ? null : gpsLatitude.value());
}
@NonNull public String getGpsLongitude() {
return StringUtils.defaultString(gpsLongitude == null ? null : gpsLongitude.value());
}
@NonNull public String credit() {
return StringUtils.defaultString(credit == null ? null : credit.value());
}
public class Values {
@Nullable private String value;
@Nullable private String source;
@Nullable private String hidden;
@Nullable public String value() {
return value;
}
@Nullable public String source() {
return source;
}
}
}

View file

@ -0,0 +1,61 @@
package fr.free.nrw.commons.wikidata.model.gallery
import com.google.gson.annotations.SerializedName
import org.apache.commons.lang3.StringUtils
class ExtMetadata {
@SerializedName("DateTime") private val dateTime: Values? = null
@SerializedName("ObjectName") private val objectName: Values? = null
@SerializedName("CommonsMetadataExtension") private val commonsMetadataExtension: Values? = null
@SerializedName("Categories") private val categories: Values? = null
@SerializedName("Assessments") private val assessments: Values? = null
@SerializedName("GPSLatitude") private val gpsLatitude: Values? = null
@SerializedName("GPSLongitude") private val gpsLongitude: Values? = null
@SerializedName("ImageDescription") private val imageDescription: Values? = null
@SerializedName("DateTimeOriginal") private val dateTimeOriginal: Values? = null
@SerializedName("Artist") private val artist: Values? = null
@SerializedName("Credit") private val credit: Values? = null
@SerializedName("Permission") private val permission: Values? = null
@SerializedName("AuthorCount") private val authorCount: Values? = null
@SerializedName("LicenseShortName") private val licenseShortName: Values? = null
@SerializedName("UsageTerms") private val usageTerms: Values? = null
@SerializedName("LicenseUrl") private val licenseUrl: Values? = null
@SerializedName("AttributionRequired") private val attributionRequired: Values? = null
@SerializedName("Copyrighted") private val copyrighted: Values? = null
@SerializedName("Restrictions") private val restrictions: Values? = null
@SerializedName("License") private val license: Values? = null
fun licenseShortName(): String = licenseShortName?.value ?: ""
fun licenseUrl(): String = licenseUrl?.value ?: ""
fun license(): String = license?.value ?: ""
fun imageDescription(): String = imageDescription?.value ?: ""
fun imageDescriptionSource(): String = imageDescription?.source ?: ""
fun objectName(): String = objectName?.value ?: ""
fun usageTerms(): String = usageTerms?.value ?: ""
fun dateTimeOriginal(): String = dateTimeOriginal?.value ?: ""
fun dateTime(): String = dateTime?.value ?: ""
fun artist(): String = artist?.value ?: ""
fun categories(): String = categories?.value ?: ""
fun gpsLatitude(): String = gpsLatitude?.value ?: ""
fun gpsLongitude(): String = gpsLongitude?.value ?: ""
fun credit(): String = credit?.value ?: ""
class Values {
val value: String? = null
val source: String? = null
val hidden: String? = null
}
}

View file

@ -1,121 +0,0 @@
package fr.free.nrw.commons.wikidata.model.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
/**
* Gson POJO for a standard image info object as returned by the API ImageInfo module
*/
public class ImageInfo implements Serializable {
private int size;
private int width;
private int height;
@Nullable private String source;
@SerializedName("thumburl") @Nullable private String thumbUrl;
@SerializedName("thumbwidth") private int thumbWidth;
@SerializedName("thumbheight") private int thumbHeight;
@SerializedName("url") @Nullable private String originalUrl;
@SerializedName("descriptionurl") @Nullable private String descriptionUrl;
@SerializedName("descriptionshorturl") @Nullable private String descriptionShortUrl;
@SerializedName("mime") @Nullable private String mimeType;
@SerializedName("extmetadata")@Nullable private ExtMetadata metadata;
@Nullable private String user;
@Nullable private String timestamp;
/**
* Query width, default width parameter of the API query in pixels.
*/
final private static int QUERY_WIDTH = 640;
/**
* Threshold height, the minimum height of the image in pixels.
*/
final private static int THRESHOLD_HEIGHT = 220;
@NonNull
public String getSource() {
return StringUtils.defaultString(source);
}
public void setSource(@Nullable String source) {
this.source = source;
}
public int getSize() {
return size;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
/**
* Get the thumbnail width.
* @return
*/
public int getThumbWidth() { return thumbWidth; }
/**
* Get the thumbnail height.
* @return
*/
public int getThumbHeight() { return thumbHeight; }
@NonNull public String getMimeType() {
return StringUtils.defaultString(mimeType, "*/*");
}
@NonNull public String getThumbUrl() {
updateThumbUrl();
return StringUtils.defaultString(thumbUrl);
}
@NonNull public String getOriginalUrl() {
return StringUtils.defaultString(originalUrl);
}
@NonNull public String getUser() {
return StringUtils.defaultString(user);
}
@NonNull public String getTimestamp() {
return StringUtils.defaultString(timestamp);
}
@Nullable public ExtMetadata getMetadata() {
return metadata;
}
/**
* Updates the ThumbUrl if image dimensions are not sufficient.
* Specifically, in panoramic images the height retrieved is less than required due to large width to height ratio,
* so we update the thumb url keeping a minimum height threshold.
*/
private void updateThumbUrl() {
// If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT
if(getThumbHeight() < THRESHOLD_HEIGHT){
// If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. )
if(getThumbWidth() == QUERY_WIDTH){
// Calculate new width depending on the aspect ratio.
final int finalWidth = (int)(THRESHOLD_HEIGHT * getThumbWidth() * 1.0 / getThumbHeight());
thumbHeight = THRESHOLD_HEIGHT;
thumbWidth = finalWidth;
final String toReplace = "/" + QUERY_WIDTH + "px";
final int position = thumbUrl.lastIndexOf(toReplace);
thumbUrl = (new StringBuilder(thumbUrl)).replace(position, position + toReplace.length(), "/" + thumbWidth + "px").toString();
}
}
}
}

View file

@ -0,0 +1,129 @@
package fr.free.nrw.commons.wikidata.model.gallery
import com.google.gson.annotations.SerializedName
import org.apache.commons.lang3.StringUtils
import java.io.Serializable
/**
* Gson POJO for a standard image info object as returned by the API ImageInfo module
*/
open class ImageInfo : Serializable {
private val size = 0
private val width = 0
private val height = 0
private var source: String? = null
@SerializedName("thumburl")
private var thumbUrl: String? = null
@SerializedName("thumbwidth")
private var thumbWidth = 0
@SerializedName("thumbheight")
private var thumbHeight = 0
@SerializedName("url")
private val originalUrl: String? = null
@SerializedName("descriptionurl")
private val descriptionUrl: String? = null
@SerializedName("descriptionshorturl")
private val descriptionShortUrl: String? = null
@SerializedName("mime")
private val mimeType: String? = null
@SerializedName("extmetadata")
private val metadata: ExtMetadata? = null
private val user: String? = null
private val timestamp: String? = null
fun getSource(): String {
return source ?: ""
}
fun setSource(source: String?) {
this.source = source
}
fun getSize(): Int {
return size
}
fun getWidth(): Int {
return width
}
fun getHeight(): Int {
return height
}
fun getThumbWidth(): Int {
return thumbWidth
}
fun getThumbHeight(): Int {
return thumbHeight
}
fun getMimeType(): String {
return mimeType ?: "*/*"
}
fun getThumbUrl(): String {
updateThumbUrl()
return thumbUrl ?: ""
}
fun getOriginalUrl(): String {
return originalUrl ?: ""
}
fun getUser(): String {
return user ?: ""
}
fun getTimestamp(): String {
return timestamp ?: ""
}
fun getMetadata(): ExtMetadata? = metadata
/**
* Updates the ThumbUrl if image dimensions are not sufficient. Specifically, in panoramic
* images the height retrieved is less than required due to large width to height ratio, so we
* update the thumb url keeping a minimum height threshold.
*/
private fun updateThumbUrl() {
// If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT
if (getThumbHeight() < THRESHOLD_HEIGHT) {
// If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. )
if (getThumbWidth() == QUERY_WIDTH) {
// Calculate new width depending on the aspect ratio.
val finalWidth = (THRESHOLD_HEIGHT * getThumbWidth() * 1.0
/ getThumbHeight()).toInt()
thumbHeight = THRESHOLD_HEIGHT
thumbWidth = finalWidth
val toReplace = "/" + QUERY_WIDTH + "px"
val position = thumbUrl!!.lastIndexOf(toReplace)
thumbUrl = (StringBuilder(thumbUrl ?: "")).replace(
position,
position + toReplace.length, "/" + thumbWidth + "px"
).toString()
}
}
}
companion object {
/**
* Query width, default width parameter of the API query in pixels.
*/
private const val QUERY_WIDTH = 640
/**
* Threshold height, the minimum height of the image in pixels.
*/
private const val THRESHOLD_HEIGHT = 220
}
}

View file

@ -1,16 +0,0 @@
package fr.free.nrw.commons.wikidata.model.gallery;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Gson POJO for a standard video info object as returned by the API VideoInfo module
*/
public class VideoInfo extends ImageInfo {
@Nullable private List<String> codecs;
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("short_name") private String shortName;
}

View file

@ -1,190 +0,0 @@
package fr.free.nrw.commons.wikidata.model.notifications;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.utils.DateUtil;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.GsonUtil;
import java.text.ParseException;
import java.util.Date;
import timber.log.Timber;
public class Notification {
@Nullable private String wiki;
private long id;
@Nullable private String type;
@Nullable private String category;
@Nullable private Title title;
@Nullable private Timestamp timestamp;
@SerializedName("*") @Nullable private Contents contents;
@NonNull public String wiki() {
return StringUtils.defaultString(wiki);
}
public long id() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public long key() {
return id + wiki().hashCode();
}
@NonNull public String type() {
return StringUtils.defaultString(type);
}
@Nullable public Title title() {
return title;
}
@Nullable public Contents getContents() {
return contents;
}
public void setContents(@Nullable final Contents contents) {
this.contents = contents;
}
@NonNull public Date getTimestamp() {
return timestamp != null ? timestamp.date() : new Date();
}
public void setTimestamp(@Nullable final Timestamp timestamp) {
this.timestamp = timestamp;
}
@NonNull String getUtcIso8601() {
return StringUtils.defaultString(timestamp != null ? timestamp.utciso8601 : null);
}
public boolean isFromWikidata() {
return wiki().equals("wikidatawiki");
}
@Override public String toString() {
return Long.toString(id);
}
public static class Title {
@Nullable private String full;
@Nullable private String text;
@NonNull public String text() {
return StringUtils.defaultString(text);
}
@NonNull public String full() {
return StringUtils.defaultString(full);
}
}
public static class Timestamp {
@Nullable private String utciso8601;
public void setUtciso8601(@Nullable final String utciso8601) {
this.utciso8601 = utciso8601;
}
public Date date() {
try {
return DateUtil.iso8601DateParse(utciso8601);
} catch (ParseException e) {
Timber.e(e);
return new Date();
}
}
}
public static class Link {
@Nullable private String url;
@Nullable private String label;
@Nullable private String tooltip;
@Nullable private String description;
@Nullable private String icon;
@NonNull public String getUrl() {
return StringUtils.defaultString(url);
}
public void setUrl(@Nullable final String url) {
this.url = url;
}
@NonNull public String getTooltip() {
return StringUtils.defaultString(tooltip);
}
@NonNull public String getLabel() {
return StringUtils.defaultString(label);
}
@NonNull public String getIcon() {
return StringUtils.defaultString(icon);
}
}
public static class Links {
@Nullable private JsonElement primary;
private Link primaryLink;
public void setPrimary(@Nullable final JsonElement primary) {
this.primary = primary;
}
@Nullable public Link getPrimary() {
if (primary == null) {
return null;
}
if (primaryLink == null && primary instanceof JsonObject) {
primaryLink = GsonUtil.INSTANCE.getDefaultGson().fromJson(primary, Link.class);
}
return primaryLink;
}
}
public static class Contents {
@Nullable private String header;
@Nullable private String compactHeader;
@Nullable private String body;
@Nullable private String icon;
@Nullable private Links links;
@NonNull public String getHeader() {
return StringUtils.defaultString(header);
}
@NonNull public String getCompactHeader() {
return StringUtils.defaultString(compactHeader);
}
public void setCompactHeader(@Nullable final String compactHeader) {
this.compactHeader = compactHeader;
}
@NonNull public String getBody() {
return StringUtils.defaultString(body);
}
@Nullable public Links getLinks() {
return links;
}
public void setLinks(@Nullable final Links links) {
this.links = links;
}
}
}

View file

@ -0,0 +1,124 @@
package fr.free.nrw.commons.wikidata.model.notifications
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.utils.DateUtil.iso8601DateParse
import fr.free.nrw.commons.wikidata.GsonUtil.defaultGson
import org.apache.commons.lang3.StringUtils
import timber.log.Timber
import java.text.ParseException
import java.util.Date
class Notification {
private val wiki: String? = null
private var id: Long = 0
private val type: String? = null
private val category: String? = null
private val title: Title? = null
private var timestamp: Timestamp? = null
@SerializedName("*")
var contents: Contents? = null
fun wiki(): String = wiki ?: ""
fun id(): Long = id
fun setId(id: Long) {
this.id = id
}
fun key(): Long =
id + wiki().hashCode()
fun type(): String =
type ?: ""
fun title(): Title? = title
fun getTimestamp(): Date =
timestamp?.date() ?: Date()
fun setTimestamp(timestamp: Timestamp?) {
this.timestamp = timestamp
}
val utcIso8601: String
get() = timestamp?.utciso8601 ?: ""
val isFromWikidata: Boolean
get() = wiki() == "wikidatawiki"
override fun toString(): String =
id.toString()
class Title {
private val full: String? = null
private val text: String? = null
fun text(): String = text ?: ""
fun full(): String = full ?: ""
}
class Timestamp {
internal var utciso8601: String? = null
fun setUtciso8601(utciso8601: String?) {
this.utciso8601 = utciso8601
}
fun date(): Date {
try {
return iso8601DateParse(utciso8601 ?: "")
} catch (e: ParseException) {
Timber.e(e)
return Date()
}
}
}
class Link {
var url: String? = null
get() = field ?: ""
val label: String? = null
get() = field ?: ""
val tooltip: String? = null
get() = field ?: ""
private val description: String? = null
val icon: String? = null
get() = field ?: ""
}
class Links {
private var primary: JsonElement? = null
private var primaryLink: Link? = null
fun setPrimary(primary: JsonElement?) {
this.primary = primary
}
fun getPrimary(): Link? {
if (primary == null) {
return null
}
if (primaryLink == null && primary is JsonObject) {
primaryLink = defaultGson.fromJson(primary, Link::class.java)
}
return primaryLink
}
}
class Contents {
val header: String? = null
get() = field ?: ""
var compactHeader: String? = null
get() = field ?: ""
val body: String? = null
get() = field ?: ""
private val icon: String? = null
var links: Links? = null
}
}

View file

@ -1,28 +0,0 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
public final class GeoMarshaller {
@Nullable
public static String marshal(@Nullable Location object) {
if (object == null) {
return null;
}
JSONObject jsonObj = new JSONObject();
try {
jsonObj.put(GeoUnmarshaller.LATITUDE, object.getLatitude());
jsonObj.put(GeoUnmarshaller.LONGITUDE, object.getLongitude());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return jsonObj.toString();
}
private GeoMarshaller() { }
}

View file

@ -1,39 +0,0 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
public final class GeoUnmarshaller {
static final String LATITUDE = "latitude";
static final String LONGITUDE = "longitude";
@Nullable
public static Location unmarshal(@Nullable String json) {
if (json == null) {
return null;
}
JSONObject jsonObj;
try {
jsonObj = new JSONObject(json);
} catch (JSONException e) {
return null;
}
return unmarshal(jsonObj);
}
@Nullable
public static Location unmarshal(@NonNull JSONObject jsonObj) {
Location ret = new Location((String) null);
ret.setLatitude(jsonObj.optDouble(LATITUDE));
ret.setLongitude(jsonObj.optDouble(LONGITUDE));
return ret;
}
private GeoUnmarshaller() { }
}

View file

@ -1,34 +1,28 @@
package fr.free.nrw.commons.wikidata.model.page;
package fr.free.nrw.commons.wikidata.model.page
import androidx.annotation.NonNull;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
import fr.free.nrw.commons.wikidata.model.EnumCode
import fr.free.nrw.commons.wikidata.model.EnumCodeMap
/** An enumeration describing the different possible namespace codes. Do not attempt to use this
* class to preserve URL path information such as Talk: or User: or localization.
* @see <a href='https://en.wikipedia.org/wiki/Wikipedia:Namespace'>Wikipedia:Namespace</a>
* @see <a href='https://www.mediawiki.org/wiki/Extension_default_namespaces'>Extension default namespaces</a>
* @see <a href='https://github.com/wikimedia/wikipedia-ios/blob/master/Wikipedia/Code/NSNumber+MWKTitleNamespace.h'>NSNumber+MWKTitleNamespace.h (iOS implementation)</a>
* @see <a href='https://www.mediawiki.org/wiki/Manual:Namespace#Built-in_namespaces'>Manual:Namespace</a>
* @see <a href='https://en.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=namespaces|namespacealiases'>Namespaces reported by API</a>
* class to preserve URL path information such as Talk: or User: or localization.
*
* @see [Wikipedia:Namespace](https://en.wikipedia.org/wiki/Wikipedia:Namespace)
* @see [Extension default namespaces](https://www.mediawiki.org/wiki/Extension_default_namespaces)
* @see [NSNumber+MWKTitleNamespace.h
* @see [Manual:Namespace](https://www.mediawiki.org/wiki/Manual:Namespace.Built-in_namespaces)
* @see [Namespaces reported by API](https://en.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=namespaces|namespacealiases)](https://github.com/wikimedia/wikipedia-ios/blob/master/Wikipedia/Code/NSNumber+MWKTitleNamespace.h)
*/
public enum Namespace implements EnumCode {
enum class Namespace(private val code: Int) : EnumCode {
MEDIA(-2),
SPECIAL(-1) {
@Override
public boolean talk() {
return false;
}
},
MAIN(0), // Main or Article
SPECIAL(-1) { override fun talk(): Boolean = false },
MAIN(0), // Main or Article
TALK(1),
USER(2),
USER_TALK(3),
PROJECT(4), // WP alias
PROJECT_TALK(5), // WT alias
FILE(6), // Image alias
FILE_TALK(7), // Image talk alias
PROJECT(4), // WP alias
PROJECT_TALK(5), // WT alias
FILE(6), // Image alias
FILE_TALK(7), // Image talk alias
MEDIAWIKI(8),
MEDIAWIKI_TALK(9),
TEMPLATE(10),
@ -137,38 +131,20 @@ public enum Namespace implements EnumCode {
GADGET_DEFINITION_TALK(2303),
TOPIC(2600);
private static final int TALK_MASK = 0x1;
private static final EnumCodeMap<Namespace> MAP = new EnumCodeMap<>(Namespace.class);
override fun code(): Int = code
private final int code;
fun special(): Boolean = this === SPECIAL
@NonNull
public static Namespace of(int code) {
return MAP.get(code);
}
fun main(): Boolean = this === MAIN
@Override
public int code() {
return code;
}
fun file(): Boolean = this === FILE
public boolean special() {
return this == SPECIAL;
}
open fun talk(): Boolean = (code and TALK_MASK) == TALK_MASK
public boolean main() {
return this == MAIN;
}
companion object {
private const val TALK_MASK = 0x1
private val MAP = EnumCodeMap(Namespace::class.java)
public boolean file() {
return this == FILE;
}
public boolean talk() {
return (code & TALK_MASK) == TALK_MASK;
}
Namespace(int code) {
this.code = code;
fun of(code: Int): Namespace = MAP[code]
}
}

View file

@ -1,156 +0,0 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Date;
/**
* Immutable class that contains metadata associated with a PageTitle.
*/
public class PageProperties implements Parcelable {
private final int pageId;
@NonNull private final Namespace namespace;
private final long revisionId;
private final Date lastModified;
private final String displayTitleText;
private final String editProtectionStatus;
private final int languageCount;
private final boolean isMainPage;
private final boolean isDisambiguationPage;
/** Nullable URL with no scheme. For example, foo.bar.com/ instead of http://foo.bar.com/. */
@Nullable private final String leadImageUrl;
@Nullable private final String leadImageName;
@Nullable private final String titlePronunciationUrl;
@Nullable private final Location geo;
@Nullable private final String wikiBaseItem;
@Nullable private final String descriptionSource;
/**
* True if the user who first requested this page can edit this page
* FIXME: This is not a true page property, since it depends on current user.
*/
private final boolean canEdit;
public int getPageId() {
return pageId;
}
public boolean isMainPage() {
return isMainPage;
}
public boolean isDisambiguationPage() {
return isDisambiguationPage;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(pageId);
parcel.writeInt(namespace.code());
parcel.writeLong(revisionId);
parcel.writeLong(lastModified.getTime());
parcel.writeString(displayTitleText);
parcel.writeString(titlePronunciationUrl);
parcel.writeString(GeoMarshaller.marshal(geo));
parcel.writeString(editProtectionStatus);
parcel.writeInt(languageCount);
parcel.writeInt(canEdit ? 1 : 0);
parcel.writeInt(isMainPage ? 1 : 0);
parcel.writeInt(isDisambiguationPage ? 1 : 0);
parcel.writeString(leadImageUrl);
parcel.writeString(leadImageName);
parcel.writeString(wikiBaseItem);
parcel.writeString(descriptionSource);
}
private PageProperties(Parcel in) {
pageId = in.readInt();
namespace = Namespace.of(in.readInt());
revisionId = in.readLong();
lastModified = new Date(in.readLong());
displayTitleText = in.readString();
titlePronunciationUrl = in.readString();
geo = GeoUnmarshaller.unmarshal(in.readString());
editProtectionStatus = in.readString();
languageCount = in.readInt();
canEdit = in.readInt() == 1;
isMainPage = in.readInt() == 1;
isDisambiguationPage = in.readInt() == 1;
leadImageUrl = in.readString();
leadImageName = in.readString();
wikiBaseItem = in.readString();
descriptionSource = in.readString();
}
public static final Parcelable.Creator<PageProperties> CREATOR
= new Parcelable.Creator<PageProperties>() {
@Override
public PageProperties createFromParcel(Parcel in) {
return new PageProperties(in);
}
@Override
public PageProperties[] newArray(int size) {
return new PageProperties[size];
}
};
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PageProperties that = (PageProperties) o;
return pageId == that.pageId
&& namespace == that.namespace
&& revisionId == that.revisionId
&& lastModified.equals(that.lastModified)
&& displayTitleText.equals(that.displayTitleText)
&& TextUtils.equals(titlePronunciationUrl, that.titlePronunciationUrl)
&& (geo == that.geo || geo != null && geo.equals(that.geo))
&& languageCount == that.languageCount
&& canEdit == that.canEdit
&& isMainPage == that.isMainPage
&& isDisambiguationPage == that.isDisambiguationPage
&& TextUtils.equals(editProtectionStatus, that.editProtectionStatus)
&& TextUtils.equals(leadImageUrl, that.leadImageUrl)
&& TextUtils.equals(leadImageName, that.leadImageName)
&& TextUtils.equals(wikiBaseItem, that.wikiBaseItem);
}
@Override
public int hashCode() {
int result = lastModified.hashCode();
result = 31 * result + displayTitleText.hashCode();
result = 31 * result + (titlePronunciationUrl != null ? titlePronunciationUrl.hashCode() : 0);
result = 31 * result + (geo != null ? geo.hashCode() : 0);
result = 31 * result + (editProtectionStatus != null ? editProtectionStatus.hashCode() : 0);
result = 31 * result + languageCount;
result = 31 * result + (isMainPage ? 1 : 0);
result = 31 * result + (isDisambiguationPage ? 1 : 0);
result = 31 * result + (leadImageUrl != null ? leadImageUrl.hashCode() : 0);
result = 31 * result + (leadImageName != null ? leadImageName.hashCode() : 0);
result = 31 * result + (wikiBaseItem != null ? wikiBaseItem.hashCode() : 0);
result = 31 * result + (canEdit ? 1 : 0);
result = 31 * result + pageId;
result = 31 * result + namespace.code();
result = 31 * result + (int) revisionId;
return result;
}
}

View file

@ -0,0 +1,156 @@
package fr.free.nrw.commons.wikidata.model.page
import android.location.Location
import android.os.Parcel
import android.os.Parcelable
import android.text.TextUtils
import org.json.JSONException
import org.json.JSONObject
import java.util.Date
/**
* Immutable class that contains metadata associated with a PageTitle.
*/
class PageProperties private constructor(parcel: Parcel) : Parcelable {
val pageId: Int = parcel.readInt()
private val namespace = Namespace.of(parcel.readInt())
private val revisionId = parcel.readLong()
private val lastModified = Date(parcel.readLong())
private val displayTitleText = parcel.readString()
private val editProtectionStatus = parcel.readString()
private val languageCount = parcel.readInt()
val isMainPage: Boolean = parcel.readInt() == 1
val isDisambiguationPage: Boolean = parcel.readInt() == 1
/** Nullable URL with no scheme. For example, foo.bar.com/ instead of http://foo.bar.com/. */
private val leadImageUrl = parcel.readString()
private val leadImageName = parcel.readString()
private val titlePronunciationUrl = parcel.readString()
private val geo = unmarshal(parcel.readString())
private val wikiBaseItem = parcel.readString()
private val descriptionSource = parcel.readString()
/**
* True if the user who first requested this page can edit this page
* FIXME: This is not a true page property, since it depends on current user.
*/
private val canEdit = parcel.readInt() == 1
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(pageId)
parcel.writeInt(namespace.code())
parcel.writeLong(revisionId)
parcel.writeLong(lastModified.time)
parcel.writeString(displayTitleText)
parcel.writeString(titlePronunciationUrl)
parcel.writeString(marshal(geo))
parcel.writeString(editProtectionStatus)
parcel.writeInt(languageCount)
parcel.writeInt(if (canEdit) 1 else 0)
parcel.writeInt(if (isMainPage) 1 else 0)
parcel.writeInt(if (isDisambiguationPage) 1 else 0)
parcel.writeString(leadImageUrl)
parcel.writeString(leadImageName)
parcel.writeString(wikiBaseItem)
parcel.writeString(descriptionSource)
}
override fun equals(o: Any?): Boolean {
if (this === o) {
return true
}
if (o == null || javaClass != o.javaClass) {
return false
}
val that = o as PageProperties
return pageId == that.pageId &&
namespace === that.namespace &&
revisionId == that.revisionId &&
lastModified == that.lastModified &&
displayTitleText == that.displayTitleText &&
TextUtils.equals(titlePronunciationUrl, that.titlePronunciationUrl) &&
(geo === that.geo || geo != null && geo == that.geo) &&
languageCount == that.languageCount &&
canEdit == that.canEdit &&
isMainPage == that.isMainPage &&
isDisambiguationPage == that.isDisambiguationPage &&
TextUtils.equals(editProtectionStatus, that.editProtectionStatus) &&
TextUtils.equals(leadImageUrl, that.leadImageUrl) &&
TextUtils.equals(leadImageName, that.leadImageName) &&
TextUtils.equals(wikiBaseItem, that.wikiBaseItem)
}
override fun hashCode(): Int {
var result = lastModified.hashCode()
result = 31 * result + displayTitleText.hashCode()
result = 31 * result + (titlePronunciationUrl?.hashCode() ?: 0)
result = 31 * result + (geo?.hashCode() ?: 0)
result = 31 * result + (editProtectionStatus?.hashCode() ?: 0)
result = 31 * result + languageCount
result = 31 * result + (if (isMainPage) 1 else 0)
result = 31 * result + (if (isDisambiguationPage) 1 else 0)
result = 31 * result + (leadImageUrl?.hashCode() ?: 0)
result = 31 * result + (leadImageName?.hashCode() ?: 0)
result = 31 * result + (wikiBaseItem?.hashCode() ?: 0)
result = 31 * result + (if (canEdit) 1 else 0)
result = 31 * result + pageId
result = 31 * result + namespace.code()
result = 31 * result + revisionId.toInt()
return result
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<PageProperties> = object : Parcelable.Creator<PageProperties> {
override fun createFromParcel(parcel: Parcel): PageProperties {
return PageProperties(parcel)
}
override fun newArray(size: Int): Array<PageProperties?> {
return arrayOfNulls(size)
}
}
}
}
private const val LATITUDE: String = "latitude"
private const val LONGITUDE: String = "longitude"
private fun marshal(location: Location?): String? {
if (location == null) {
return null
}
val jsonObj = JSONObject().apply {
try {
put(LATITUDE, location.latitude)
put(LONGITUDE, location.longitude)
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
return jsonObj.toString()
}
private fun unmarshal(json: String?): Location? {
if (json == null) {
return null
}
return try {
val jsonObject = JSONObject(json)
Location(null as String?).apply {
latitude = jsonObject.optDouble(LATITUDE)
longitude = jsonObject.optDouble(LONGITUDE)
}
} catch (e: JSONException) {
null
}
}

View file

@ -1,339 +0,0 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Locale;
import timber.log.Timber;
/**
* Represents certain vital information about a page, including the title, namespace,
* and fragment (section anchor target). It can also contain a thumbnail URL for the
* page, and a short description retrieved from Wikidata.
*
* WARNING: This class is not immutable! Specifically, the thumbnail URL and the Wikidata
* description can be altered after construction. Therefore do NOT rely on all the fields
* of a PageTitle to remain constant for the lifetime of the object.
*/
public class PageTitle implements Parcelable {
public static final Parcelable.Creator<PageTitle> CREATOR
= new Parcelable.Creator<PageTitle>() {
@Override
public PageTitle createFromParcel(Parcel in) {
return new PageTitle(in);
}
@Override
public PageTitle[] newArray(int size) {
return new PageTitle[size];
}
};
/**
* The localised namespace of the page as a string, or null if the page is in mainspace.
*
* This field contains the prefix of the page's title, as opposed to the namespace ID used by
* MediaWiki. Therefore, mainspace pages always have a null namespace, as they have no prefix,
* and the namespace of a page will depend on the language of the wiki the user is currently
* looking at.
*
* Examples:
* * [[Manchester]] on enwiki will have a namespace of null
* * [[Deutschland]] on dewiki will have a namespace of null
* * [[User:Deskana]] on enwiki will have a namespace of "User"
* * [[Utilisateur:Deskana]] on frwiki will have a namespace of "Utilisateur", even if you got
* to the page by going to [[User:Deskana]] and having MediaWiki automatically redirect you.
*/
// TODO: remove. This legacy code is the localized namespace name (File, Special, Talk, etc) but
// isn't consistent across titles. e.g., articles with colons, such as RTÉ News: Six One,
// are broken.
@Nullable private final String namespace;
@NonNull private final String text;
@Nullable private final String fragment;
@Nullable private String thumbUrl;
@SerializedName("site") @NonNull private final WikiSite wiki;
@Nullable private String description;
@Nullable private final PageProperties properties;
// TODO: remove after the restbase endpoint supports ZH variants.
@Nullable private String convertedText;
/**
* Creates a new PageTitle object.
* Use this if you want to pass in a fragment portion separately from the title.
*
* @param prefixedText title of the page with optional namespace prefix
* @param fragment optional fragment portion
* @param wiki the wiki site the page belongs to
* @return a new PageTitle object matching the given input parameters
*/
public static PageTitle withSeparateFragment(@NonNull String prefixedText,
@Nullable String fragment, @NonNull WikiSite wiki) {
if (TextUtils.isEmpty(fragment)) {
return new PageTitle(prefixedText, wiki, null, (PageProperties) null);
} else {
// TODO: this class needs some refactoring to allow passing in a fragment
// without having to do string manipulations.
return new PageTitle(prefixedText + "#" + fragment, wiki, null, (PageProperties) null);
}
}
public PageTitle(@Nullable final String namespace, @NonNull String text, @Nullable String fragment, @Nullable String thumbUrl, @NonNull WikiSite wiki) {
this.namespace = namespace;
this.text = text;
this.fragment = fragment;
this.wiki = wiki;
this.thumbUrl = thumbUrl;
properties = null;
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description, @Nullable PageProperties properties) {
this(text, wiki, thumbUrl, properties);
this.description = description;
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description) {
this(text, wiki, thumbUrl);
this.description = description;
}
public PageTitle(@Nullable String namespace, @NonNull String text, @NonNull WikiSite wiki) {
this(namespace, text, null, null, wiki);
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl) {
this(text, wiki, thumbUrl, (PageProperties) null);
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki) {
this(text, wiki, null);
}
private PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl,
@Nullable PageProperties properties) {
if (text == null) {
text = "";
}
// FIXME: Does not handle mainspace articles with a colon in the title well at all
String[] fragParts = text.split("#", -1);
text = fragParts[0];
if (fragParts.length > 1) {
this.fragment = decodeURL(fragParts[1]).replace(" ", "_");
} else {
this.fragment = null;
}
String[] parts = text.split(":", -1);
if (parts.length > 1) {
String namespaceOrLanguage = parts[0];
if (Arrays.asList(Locale.getISOLanguages()).contains(namespaceOrLanguage)) {
this.namespace = null;
this.wiki = new WikiSite(wiki.authority(), namespaceOrLanguage);
} else {
this.wiki = wiki;
this.namespace = namespaceOrLanguage;
}
this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.length));
} else {
this.wiki = wiki;
this.namespace = null;
this.text = parts[0];
}
this.thumbUrl = thumbUrl;
this.properties = properties;
}
/**
* Decodes a URL-encoded string into its UTF-8 equivalent. If the string cannot be decoded, the
* original string is returned.
* @param url The URL-encoded string that you wish to decode.
* @return The decoded string, or the input string if the decoding failed.
*/
@NonNull private String decodeURL(@NonNull String url) {
try {
return URLDecoder.decode(url, "UTF-8");
} catch (IllegalArgumentException e) {
// Swallow IllegalArgumentException (can happen with malformed encoding), and just
// return the original string.
Timber.d("URL decoding failed. String was: %s", url);
return url;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@NonNull public WikiSite getWikiSite() {
return wiki;
}
@NonNull public String getText() {
return text.replace(" ", "_");
}
@Nullable public String getFragment() {
return fragment;
}
@Nullable public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(@Nullable String thumbUrl) {
this.thumbUrl = thumbUrl;
}
@Nullable public String getDescription() {
return description;
}
public void setDescription(@Nullable String description) {
this.description = description;
}
@NonNull
public String getConvertedText() {
return convertedText == null ? getPrefixedText() : convertedText;
}
public void setConvertedText(@Nullable String convertedText) {
this.convertedText = convertedText;
}
@NonNull public String getDisplayText() {
return getPrefixedText().replace("_", " ");
}
@NonNull public String getDisplayTextWithoutNamespace() {
return text.replace("_", " ");
}
public boolean hasProperties() {
return properties != null;
}
@Nullable public PageProperties getProperties() {
return properties;
}
public boolean isMainPage() {
return properties != null && properties.isMainPage();
}
public boolean isDisambiguationPage() {
return properties != null && properties.isDisambiguationPage();
}
public String getCanonicalUri() {
return getUriForDomain(getWikiSite().authority());
}
public String getMobileUri() {
return getUriForDomain(getWikiSite().mobileAuthority());
}
public String getUriForAction(String action) {
try {
return String.format(
"%1$s://%2$s/w/index.php?title=%3$s&action=%4$s",
getWikiSite().scheme(),
getWikiSite().authority(),
URLEncoder.encode(getPrefixedText(), "utf-8"),
action
);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public String getPrefixedText() {
// TODO: find a better way to check if the namespace is a ISO Alpha2 Code (two digits country code)
return namespace == null ? getText() : addUnderscores(namespace) + ":" + getText();
}
private String addUnderscores(@NonNull String text) {
return text.replace(" ", "_");
}
@Override public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(namespace);
parcel.writeString(text);
parcel.writeString(fragment);
parcel.writeParcelable(wiki, flags);
parcel.writeParcelable(properties, flags);
parcel.writeString(thumbUrl);
parcel.writeString(description);
parcel.writeString(convertedText);
}
@Override public boolean equals(Object o) {
if (!(o instanceof PageTitle)) {
return false;
}
PageTitle other = (PageTitle)o;
// Not using namespace directly since that can be null
return normalizedEquals(other.getPrefixedText(), getPrefixedText()) && other.wiki.equals(wiki);
}
// Compare two strings based on their normalized form, using the Unicode Normalization Form C.
// This should be used when comparing or verifying strings that will be exchanged between
// different platforms (iOS, desktop, etc) that may encode strings using inconsistent
// composition, especially for accents, diacritics, etc.
private boolean normalizedEquals(@Nullable String str1, @Nullable String str2) {
if (str1 == null || str2 == null) {
return (str1 == null && str2 == null);
}
return Normalizer.normalize(str1, Normalizer.Form.NFC)
.equals(Normalizer.normalize(str2, Normalizer.Form.NFC));
}
@Override public int hashCode() {
int result = getPrefixedText().hashCode();
result = 31 * result + wiki.hashCode();
return result;
}
@Override public String toString() {
return getPrefixedText();
}
@Override public int describeContents() {
return 0;
}
private String getUriForDomain(String domain) {
try {
return String.format(
"%1$s://%2$s/wiki/%3$s%4$s",
getWikiSite().scheme(),
domain,
URLEncoder.encode(getPrefixedText(), "utf-8"),
(this.fragment != null && this.fragment.length() > 0) ? ("#" + this.fragment) : ""
);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private PageTitle(Parcel in) {
namespace = in.readString();
text = in.readString();
fragment = in.readString();
wiki = in.readParcelable(WikiSite.class.getClassLoader());
properties = in.readParcelable(PageProperties.class.getClassLoader());
thumbUrl = in.readString();
description = in.readString();
convertedText = in.readString();
}
}

View file

@ -0,0 +1,284 @@
package fr.free.nrw.commons.wikidata.model.page
import android.os.Parcel
import android.os.Parcelable
import android.text.TextUtils
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.wikidata.model.WikiSite
import timber.log.Timber
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
import java.text.Normalizer
import java.util.Arrays
import java.util.Locale
/**
* Represents certain vital information about a page, including the title, namespace,
* and fragment (section anchor target). It can also contain a thumbnail URL for the
* page, and a short description retrieved from Wikidata.
*
* WARNING: This class is not immutable! Specifically, the thumbnail URL and the Wikidata
* description can be altered after construction. Therefore do NOT rely on all the fields
* of a PageTitle to remain constant for the lifetime of the object.
*/
class PageTitle : Parcelable {
/**
* The localised namespace of the page as a string, or null if the page is in mainspace.
*
* This field contains the prefix of the page's title, as opposed to the namespace ID used by
* MediaWiki. Therefore, mainspace pages always have a null namespace, as they have no prefix,
* and the namespace of a page will depend on the language of the wiki the user is currently
* looking at.
*
* Examples:
* * [[Manchester]] on enwiki will have a namespace of null
* * [[Deutschland]] on dewiki will have a namespace of null
* * [[User:Deskana]] on enwiki will have a namespace of "User"
* * [[Utilisateur:Deskana]] on frwiki will have a namespace of "Utilisateur", even if you got
* to the page by going to [[User:Deskana]] and having MediaWiki automatically redirect you.
*/
// TODO: remove. This legacy code is the localized namespace name (File, Special, Talk, etc) but
// isn't consistent across titles. e.g., articles with colons, such as RTÉ News: Six One,
// are broken.
private val namespace: String?
private val text: String
val fragment: String?
var thumbUrl: String?
@SerializedName("site")
val wikiSite: WikiSite
var description: String? = null
private val properties: PageProperties?
// TODO: remove after the restbase endpoint supports ZH variants.
private var convertedText: String? = null
constructor(namespace: String?, text: String, fragment: String?, thumbUrl: String?, wiki: WikiSite) {
this.namespace = namespace
this.text = text
this.fragment = fragment
this.thumbUrl = thumbUrl
wikiSite = wiki
properties = null
}
constructor(text: String?, wiki: WikiSite, thumbUrl: String?, description: String?, properties: PageProperties?) : this(text, wiki, thumbUrl, properties) {
this.description = description
}
constructor(text: String?, wiki: WikiSite, thumbUrl: String?, description: String?) : this(text, wiki, thumbUrl) {
this.description = description
}
constructor(namespace: String?, text: String, wiki: WikiSite) : this(namespace, text, null, null, wiki)
@JvmOverloads
constructor(text: String?, wiki: WikiSite, thumbUrl: String? = null) : this(text, wiki, thumbUrl, null as PageProperties?)
private constructor(input: String?, wiki: WikiSite, thumbUrl: String?, properties: PageProperties?) {
var text = input ?: ""
// FIXME: Does not handle mainspace articles with a colon in the title well at all
val fragParts = text.split("#".toRegex()).toTypedArray()
text = fragParts[0]
fragment = if (fragParts.size > 1) {
decodeURL(fragParts[1]).replace(" ", "_")
} else {
null
}
val parts = text.split(":".toRegex()).toTypedArray()
if (parts.size > 1) {
val namespaceOrLanguage = parts[0]
if (Arrays.asList(*Locale.getISOLanguages()).contains(namespaceOrLanguage)) {
namespace = null
wikiSite = WikiSite(wiki.authority(), namespaceOrLanguage)
} else {
wikiSite = wiki
namespace = namespaceOrLanguage
}
this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.size))
} else {
wikiSite = wiki
namespace = null
this.text = parts[0]
}
this.thumbUrl = thumbUrl
this.properties = properties
}
/**
* Decodes a URL-encoded string into its UTF-8 equivalent. If the string cannot be decoded, the
* original string is returned.
* @param url The URL-encoded string that you wish to decode.
* @return The decoded string, or the input string if the decoding failed.
*/
private fun decodeURL(url: String): String {
try {
return URLDecoder.decode(url, "UTF-8")
} catch (e: IllegalArgumentException) {
// Swallow IllegalArgumentException (can happen with malformed encoding), and just
// return the original string.
Timber.d("URL decoding failed. String was: %s", url)
return url
} catch (e: UnsupportedEncodingException) {
throw RuntimeException(e)
}
}
private fun getTextWithoutSpaces(): String =
text.replace(" ", "_")
fun getConvertedText(): String =
if (convertedText == null) prefixedText else convertedText!!
fun setConvertedText(convertedText: String?) {
this.convertedText = convertedText
}
val displayText: String
get() = prefixedText.replace("_", " ")
val displayTextWithoutNamespace: String
get() = text.replace("_", " ")
fun hasProperties(): Boolean =
properties != null
val isMainPage: Boolean
get() = properties != null && properties.isMainPage
val isDisambiguationPage: Boolean
get() = properties != null && properties.isDisambiguationPage
val canonicalUri: String
get() = getUriForDomain(wikiSite.authority())
val mobileUri: String
get() = getUriForDomain(wikiSite.mobileAuthority())
fun getUriForAction(action: String?): String {
try {
return String.format(
"%1\$s://%2\$s/w/index.php?title=%3\$s&action=%4\$s",
wikiSite.scheme(),
wikiSite.authority(),
URLEncoder.encode(prefixedText, "utf-8"),
action
)
} catch (e: UnsupportedEncodingException) {
throw RuntimeException(e)
}
}
// TODO: find a better way to check if the namespace is a ISO Alpha2 Code (two digits country code)
val prefixedText: String
get() = namespace?.let { addUnderscores(it) + ":" + getTextWithoutSpaces() }
?: getTextWithoutSpaces()
private fun addUnderscores(text: String): String =
text.replace(" ", "_")
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(namespace)
parcel.writeString(text)
parcel.writeString(fragment)
parcel.writeParcelable(wikiSite, flags)
parcel.writeParcelable(properties, flags)
parcel.writeString(thumbUrl)
parcel.writeString(description)
parcel.writeString(convertedText)
}
override fun equals(o: Any?): Boolean {
if (o !is PageTitle) {
return false
}
val other = o
// Not using namespace directly since that can be null
return normalizedEquals(other.prefixedText, prefixedText) && other.wikiSite.equals(wikiSite)
}
// Compare two strings based on their normalized form, using the Unicode Normalization Form C.
// This should be used when comparing or verifying strings that will be exchanged between
// different platforms (iOS, desktop, etc) that may encode strings using inconsistent
// composition, especially for accents, diacritics, etc.
private fun normalizedEquals(str1: String?, str2: String?): Boolean {
if (str1 == null || str2 == null) {
return (str1 == null && str2 == null)
}
return (Normalizer.normalize(str1, Normalizer.Form.NFC)
== Normalizer.normalize(str2, Normalizer.Form.NFC))
}
override fun hashCode(): Int {
var result = prefixedText.hashCode()
result = 31 * result + wikiSite.hashCode()
return result
}
override fun toString(): String =
prefixedText
override fun describeContents(): Int = 0
private fun getUriForDomain(domain: String): String = try {
String.format(
"%1\$s://%2\$s/wiki/%3\$s%4\$s",
wikiSite.scheme(),
domain,
URLEncoder.encode(prefixedText, "utf-8"),
if ((fragment != null && fragment.length > 0)) ("#$fragment") else ""
)
} catch (e: UnsupportedEncodingException) {
throw RuntimeException(e)
}
private constructor(parcel: Parcel) {
namespace = parcel.readString()
text = parcel.readString()!!
fragment = parcel.readString()
wikiSite = parcel.readParcelable(WikiSite::class.java.classLoader)!!
properties = parcel.readParcelable(PageProperties::class.java.classLoader)
thumbUrl = parcel.readString()
description = parcel.readString()
convertedText = parcel.readString()
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<PageTitle> = object : Parcelable.Creator<PageTitle> {
override fun createFromParcel(parcel: Parcel): PageTitle {
return PageTitle(parcel)
}
override fun newArray(size: Int): Array<PageTitle?> {
return arrayOfNulls(size)
}
}
/**
* Creates a new PageTitle object.
* Use this if you want to pass in a fragment portion separately from the title.
*
* @param prefixedText title of the page with optional namespace prefix
* @param fragment optional fragment portion
* @param wiki the wiki site the page belongs to
* @return a new PageTitle object matching the given input parameters
*/
fun withSeparateFragment(
prefixedText: String,
fragment: String?, wiki: WikiSite
): PageTitle {
return if (TextUtils.isEmpty(fragment)) {
PageTitle(prefixedText, wiki, null, null as PageProperties?)
} else {
// TODO: this class needs some refactoring to allow passing in a fragment
// without having to do string manipulations.
PageTitle("$prefixedText#$fragment", wiki, null, null as PageProperties?)
}
}
}
}

View file

@ -42,22 +42,22 @@ class MediaConverterTest {
@Test
fun testConvertIfThumbUrlBlank() {
Mockito.`when`(imageInfo.metadata).thenReturn(metadata)
Mockito.`when`(imageInfo.thumbUrl).thenReturn("")
Mockito.`when`(imageInfo.originalUrl).thenReturn("originalUrl")
Mockito.`when`(imageInfo.metadata?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.metadata?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("")
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals(media.thumbUrl, media.imageUrl, "originalUrl")
}
@Test
fun testConvertIfThumbUrlNotBlank() {
Mockito.`when`(imageInfo.metadata).thenReturn(metadata)
Mockito.`when`(imageInfo.thumbUrl).thenReturn("thumbUrl")
Mockito.`when`(imageInfo.originalUrl).thenReturn("originalUrl")
Mockito.`when`(imageInfo.metadata?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.metadata?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals(media.thumbUrl, "thumbUrl")
}

View file

@ -123,11 +123,11 @@ class NotificationClientTest {
setTimestamp(Notification.Timestamp().apply { setUtciso8601(timestamp) })
contents = Notification.Contents().apply {
setCompactHeader(compactHeader)
this.compactHeader = compactHeader
links = Notification.Links().apply {
setPrimary(GsonUtil.defaultGson.toJsonTree(
Notification.Link().apply { setUrl(primaryUrl) }
Notification.Link().apply { url = primaryUrl }
))
}
}