package org.wikipedia.page;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import org.json.JSONException;
import org.json.JSONObject;
import org.wikipedia.WikipediaApp;
import org.wikipedia.crash.RemoteLogException;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.staticdata.MainPageNameData;
import org.wikipedia.util.StringUtil;
import org.wikipedia.util.log.L;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import static org.wikipedia.util.StringUtil.md5string;
import static org.wikipedia.util.UriUtil.decodeURL;
/**
* 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 {
private static final String LANGUAGE_CODE_KEY = "languageCode";
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;
/**
* 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) {
// FIXME: Does not handle mainspace articles with a colon in the title well at all
if (TextUtils.isEmpty(text)) {
// If empty, this refers to the main page.
text = MainPageNameData.valueFor(wiki.languageCode());
}
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) {
this.namespace = parts[0];
this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.length));
} else {
this.namespace = null;
this.text = parts[0];
}
this.thumbUrl = thumbUrl;
this.wiki = wiki;
this.properties = properties;
}
public PageTitle(JSONObject json) {
this.namespace = json.optString("namespace", null);
this.text = json.optString("text", null);
this.fragment = json.optString("fragment", null);
if (json.has("site")) {
if (json.has(LANGUAGE_CODE_KEY)) {
wiki = new WikiSite(json.optString("site"), json.optString(LANGUAGE_CODE_KEY));
} else {
// TODO: remove in September 2016.
wiki = new WikiSite(json.optString("site"));
}
} else {
L.logRemoteErrorIfProd(new RemoteLogException("wiki is null").put("json", json.toString()));
wiki = WikipediaApp.getInstance().getWikiSite();
}
this.properties = json.has("properties") ? new PageProperties(json.optJSONObject("properties")) : null;
this.thumbUrl = json.optString("thumbUrl", null);
this.description = json.optString("description", null);
}
@Nullable
public String getNamespace() {
return namespace;
}
@NonNull public Namespace namespace() {
if (properties != null) {
return properties.getNamespace();
}
// Properties has the accurate namespace but it doesn't exist. Guess based on title.
return Namespace.fromLegacyString(wiki, namespace);
}
@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 getDisplayText() {
return getPrefixedText().replace("_", " ");
}
public boolean hasProperties() {
return properties != null;
}
@Nullable public PageProperties getProperties() {
return properties;
}
public boolean isMainPage() {
if (properties != null) {
return properties.isMainPage();
}
String mainPageTitle = MainPageNameData.valueFor(getWikiSite().languageCode());
return mainPageTitle.equals(getDisplayText());
}
public boolean isDisambiguationPage() {
return properties != null && properties.isDisambiguationPage();
}
/** Please keep the ID stable. */
public String getIdentifier() {
return md5string(toIdentifierJSON().toString());
}
public JSONObject toJSON() {
try {
JSONObject json = toIdentifierJSON();
json.put(LANGUAGE_CODE_KEY, wiki.languageCode());
if (properties != null) {
json.put("properties", properties.toJSON());
}
json.put("thumbUrl", getThumbUrl());
json.put("description", getDescription());
return json;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
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() {
return namespace == null ? getText() : StringUtil.addUnderscores(namespace) + ":" + getText();
}
/**
* Check if the Title represents a File:
*
* @return true if it is a File page, false if not
*/
public boolean isFilePage() {
return namespace().file();
}
/**
* Check if the Title represents a special page
*
* @return true if it is a special page, false if not
*/
public boolean isSpecial() {
return namespace().special();
}
/**
* Check if the Title represents a talk page
*
* @return true if it is a talk page, false if not
*/
public boolean isTalkPage() {
return namespace().talk();
}
@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);
}
@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 other.getPrefixedText().equals(getPrefixedText()) && other.wiki.equals(wiki);
}
@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;
}
/** Please keep the ID stable. */
private JSONObject toIdentifierJSON() {
try {
JSONObject json = new JSONObject();
json.put("namespace", getNamespace());
json.put("text", getText());
json.put("fragment", getFragment());
json.put("site", wiki.authority());
return json;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
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();
}
}