blob: ee96aaa5fec491fd536ebb7ae26e8bd3c9128582 [file] [log] [blame]
package org.wordpress.android.models;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.wordpress.android.ui.reader.ReaderConstants;
import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
import org.wordpress.android.ui.reader.utils.ImageSizeMap;
import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
import org.wordpress.android.ui.reader.utils.ReaderUtils;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.GravatarUtils;
import org.wordpress.android.util.HtmlUtils;
import org.wordpress.android.util.JSONUtils;
import org.wordpress.android.util.StringUtils;
import java.text.BreakIterator;
import java.util.Iterator;
public class ReaderPost {
private String pseudoId;
public long postId;
public long blogId;
public long feedId;
public long feedItemId;
public long authorId;
private String title;
private String text;
private String excerpt;
private String authorName;
private String authorFirstName;
private String blogName;
private String blogUrl;
private String postAvatar;
private String primaryTag; // most popular tag on this post based on usage in blog
private String secondaryTag; // second most popular tag on this post based on usage in blog
/*
* the "date" field is a generic date which depends on the stream the post is from:
* - for tagged posts, this is the date the post was tagged
* - for liked posts, this is the date the post was liked
* - for other posts, this is the date the post was published
* this date is used when requesting older posts from the backend, and is also used
* to generate the sortIndex below (which determines how posts are sorted for display)
*/
private String date;
private String pubDate;
public double sortIndex;
private String url;
private String shortUrl;
private String featuredImage;
private String featuredVideo;
public int numReplies; // includes comments, trackbacks & pingbacks
public int numLikes;
public boolean isLikedByCurrentUser;
public boolean isFollowedByCurrentUser;
public boolean isCommentsOpen;
public boolean isExternal;
public boolean isPrivate;
public boolean isVideoPress;
public boolean isJetpack;
private String attachmentsJson;
private String discoverJson;
private String format;
public long xpostPostId;
public long xpostBlogId;
private String railcarJson;
public static ReaderPost fromJson(JSONObject json) {
if (json == null) {
throw new IllegalArgumentException("null json post");
}
ReaderPost post = new ReaderPost();
post.postId = json.optLong("ID");
post.blogId = json.optLong("site_ID");
post.feedId = json.optLong("feed_ID");
post.feedItemId = json.optLong("feed_item_ID");
if (json.has("pseudo_ID")) {
post.pseudoId = JSONUtils.getString(json, "pseudo_ID"); // read/ endpoint
} else {
post.pseudoId = JSONUtils.getString(json, "global_ID"); // sites/ endpoint
}
// remove HTML from the excerpt
post.excerpt = HtmlUtils.fastStripHtml(JSONUtils.getString(json, "excerpt")).trim();
post.text = JSONUtils.getString(json, "content");
post.title = JSONUtils.getStringDecoded(json, "title");
post.format = JSONUtils.getString(json, "format");
post.url = JSONUtils.getString(json, "URL");
post.shortUrl = JSONUtils.getString(json, "short_URL");
post.setBlogUrl(JSONUtils.getString(json, "site_URL"));
post.numLikes = json.optInt("like_count");
post.isLikedByCurrentUser = JSONUtils.getBool(json, "i_like");
post.isFollowedByCurrentUser = JSONUtils.getBool(json, "is_following");
post.isExternal = JSONUtils.getBool(json, "is_external");
post.isPrivate = JSONUtils.getBool(json, "site_is_private");
post.isJetpack = JSONUtils.getBool(json, "is_jetpack");
JSONObject jsonDiscussion = json.optJSONObject("discussion");
if (jsonDiscussion != null) {
post.isCommentsOpen = JSONUtils.getBool(jsonDiscussion, "comments_open");
post.numReplies = jsonDiscussion.optInt("comment_count");
} else {
post.isCommentsOpen = JSONUtils.getBool(json, "comments_open");
post.numReplies = json.optInt("comment_count");
}
// parse the author section
assignAuthorFromJson(post, json.optJSONObject("author"));
post.featuredImage = JSONUtils.getString(json, "featured_image");
post.blogName = JSONUtils.getStringDecoded(json, "site_name");
post.pubDate = JSONUtils.getString(json, "date");
// a post's date is the liked date for liked posts, tagged date for tag streams, and
// published date for all others
if (json.has("date_liked")) {
post.date = JSONUtils.getString(json, "date_liked");
} else if (json.has("tagged_on")) {
post.date = JSONUtils.getString(json, "tagged_on");
} else {
post.date = post.pubDate;
}
// sort index determines how posts are sorted, which is based on the date retrieved above
post.sortIndex = DateTimeUtils.timestampFromIso8601(post.date);
// if the post is untitled, make up a title from the excerpt
if (!post.hasTitle() && post.hasExcerpt()) {
post.title = extractTitle(post.excerpt, 50);
}
// remove html from title (rare, but does happen)
if (post.hasTitle() && post.title.contains("<") && post.title.contains(">")) {
post.title = HtmlUtils.stripHtml(post.title);
}
// parse the tags section
assignTagsFromJson(post, json.optJSONObject("tags"));
// parse the attachments
JSONObject jsonAttachments = json.optJSONObject("attachments");
if (jsonAttachments != null && jsonAttachments.length() > 0) {
post.attachmentsJson = jsonAttachments.toString();
}
// site metadata - returned when ?meta=site was added to the request
JSONObject jsonSite = JSONUtils.getJSONChild(json, "meta/data/site");
if (jsonSite != null) {
post.blogId = jsonSite.optInt("ID");
post.blogName = JSONUtils.getString(jsonSite, "name");
post.setBlogUrl(JSONUtils.getString(jsonSite, "URL"));
post.isPrivate = JSONUtils.getBool(jsonSite, "is_private");
// TODO: as of 29-Sept-2014, this is broken - endpoint returns false when it should be true
post.isJetpack = JSONUtils.getBool(jsonSite, "jetpack");
}
// "discover" posts
JSONObject jsonDiscover = json.optJSONObject("discover_metadata");
if (jsonDiscover != null) {
post.setDiscoverJson(jsonDiscover.toString());
}
// xpost info
assignXpostIdsFromJson(post, json.optJSONArray("metadata"));
// if there's no featured image, check if featured media has been set - this is sometimes
// a YouTube or Vimeo video, in which case store it as the featured video so we can treat
// it as a video
if (!post.hasFeaturedImage()) {
JSONObject jsonMedia = json.optJSONObject("featured_media");
if (jsonMedia != null && jsonMedia.length() > 0) {
String mediaUrl = JSONUtils.getString(jsonMedia, "uri");
if (!TextUtils.isEmpty(mediaUrl)) {
String type = JSONUtils.getString(jsonMedia, "type");
boolean isVideo = (type != null && type.equals("video"));
if (isVideo) {
post.featuredVideo = mediaUrl;
} else {
post.featuredImage = mediaUrl;
}
}
}
}
// if the post still doesn't have a featured image but we have attachment data, check whether
// we can find a suitable featured image from the attachments
if (!post.hasFeaturedImage() && post.hasAttachments()) {
post.featuredImage = new ImageSizeMap(post.attachmentsJson)
.getLargestImageUrl(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
}
// if we *still* don't have a featured image but the text contains an IMG tag, check whether
// we can find a suitable image from the text
if (!post.hasFeaturedImage() && post.hasText() && post.text.contains("<img")) {
post.featuredImage = new ReaderImageScanner(post.text, post.isPrivate)
.getLargestImage(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
}
// "railcar" data - currently used in search streams, used by TrainTracks
JSONObject jsonRailcar = json.optJSONObject("railcar");
if (jsonRailcar != null) {
post.setRailcarJson(jsonRailcar.toString());
}
return post;
}
/*
* assigns cross post blog & post IDs from post's metadata section
* "metadata": [
* {
* "id": "21192",
* "key": "xpost_origin",
* "value": "11326809:18427"
* }
* ],
*/
private static void assignXpostIdsFromJson(ReaderPost post, JSONArray jsonMetadata) {
if (jsonMetadata == null) return;
for (int i = 0; i < jsonMetadata.length(); i++) {
JSONObject jsonMetaItem = jsonMetadata.optJSONObject(i);
String metaKey = jsonMetaItem.optString("key");
if (!TextUtils.isEmpty(metaKey) && metaKey.equals("xpost_origin")) {
String value = jsonMetaItem.optString("value");
if (!TextUtils.isEmpty(value) && value.contains(":")) {
String[] valuePair = value.split(":");
if (valuePair.length == 2) {
post.xpostBlogId = StringUtils.stringToLong(valuePair[0]);
post.xpostPostId = StringUtils.stringToLong(valuePair[1]);
return;
}
}
}
}
}
/*
* assigns author-related info to the passed post from the passed JSON "author" object
*/
private static void assignAuthorFromJson(ReaderPost post, JSONObject jsonAuthor) {
if (jsonAuthor == null) return;
post.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
post.authorFirstName = JSONUtils.getStringDecoded(jsonAuthor, "first_name");
post.postAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL");
post.authorId = jsonAuthor.optLong("ID");
// site_URL doesn't exist for /sites/ endpoints, so get it from the author
if (TextUtils.isEmpty(post.blogUrl)) {
post.setBlogUrl(JSONUtils.getString(jsonAuthor, "URL"));
}
}
/*
* assigns primary/secondary tags to the passed post from the passed JSON "tags" object
*/
private static void assignTagsFromJson(ReaderPost post, JSONObject jsonTags) {
if (jsonTags == null) {
return;
}
Iterator<String> it = jsonTags.keys();
if (!it.hasNext()) {
return;
}
// most popular tag & second most popular tag, based on usage count on this blog
String mostPopularTag = null;
String nextMostPopularTag = null;
int popularCount = 0;
while (it.hasNext()) {
JSONObject jsonThisTag = jsonTags.optJSONObject(it.next());
// if the number of posts on this blog that use this tag is higher than previous,
// set this as the most popular tag, and set the second most popular tag to
// the current most popular tag
int postCount = jsonThisTag.optInt("post_count");
if (postCount > popularCount) {
nextMostPopularTag = mostPopularTag;
mostPopularTag = JSONUtils.getStringDecoded(jsonThisTag, "slug");
popularCount = postCount;
}
}
// don't set primary tag if one is already set
if (!post.hasPrimaryTag()) {
post.setPrimaryTag(mostPopularTag);
}
post.setSecondaryTag(nextMostPopularTag);
}
/*
* extracts a title from a post's excerpt - used when the post has no title
*/
private static String extractTitle(final String excerpt, int maxLen) {
if (TextUtils.isEmpty(excerpt))
return null;
if (excerpt.length() < maxLen)
return excerpt.trim();
StringBuilder result = new StringBuilder();
BreakIterator wordIterator = BreakIterator.getWordInstance();
wordIterator.setText(excerpt);
int start = wordIterator.first();
int end = wordIterator.next();
int totalLen = 0;
while (end != BreakIterator.DONE) {
String word = excerpt.substring(start, end);
result.append(word);
totalLen += word.length();
if (totalLen >= maxLen)
break;
start = end;
end = wordIterator.next();
}
if (totalLen==0)
return null;
return result.toString().trim() + "...";
}
// --------------------------------------------------------------------------------------------
public String getAuthorName() {
return StringUtils.notNullStr(authorName);
}
public void setAuthorName(String name) {
this.authorName = StringUtils.notNullStr(name);
}
public String getAuthorFirstName() {
return StringUtils.notNullStr(authorFirstName);
}
public void setAuthorFirstName(String name) {
this.authorFirstName = StringUtils.notNullStr(name);
}
public String getTitle() {
return StringUtils.notNullStr(title);
}
public void setTitle(String title) {
this.title = StringUtils.notNullStr(title);
}
public String getText() {
return StringUtils.notNullStr(text);
}
public void setText(String text) {
this.text = StringUtils.notNullStr(text);
}
public String getExcerpt() {
return StringUtils.notNullStr(excerpt);
}
public void setExcerpt(String excerpt) {
this.excerpt = StringUtils.notNullStr(excerpt);
}
// https://codex.wordpress.org/Post_Formats
public String getFormat() {
return StringUtils.notNullStr(format);
}
public void setFormat(String format) {
this.format = StringUtils.notNullStr(format);
}
public boolean isGallery() {
return format != null && format.equals("gallery");
}
public String getUrl() {
return StringUtils.notNullStr(url);
}
public void setUrl(String url) {
this.url = StringUtils.notNullStr(url);
}
public String getShortUrl() {
return StringUtils.notNullStr(shortUrl);
}
public void setShortUrl(String url) {
this.shortUrl = StringUtils.notNullStr(url);
}
public boolean hasShortUrl() {
return !TextUtils.isEmpty(shortUrl);
}
public String getFeaturedImage() {
return StringUtils.notNullStr(featuredImage);
}
public void setFeaturedImage(String featuredImage) {
this.featuredImage = StringUtils.notNullStr(featuredImage);
}
public String getFeaturedVideo() {
return StringUtils.notNullStr(featuredVideo);
}
public void setFeaturedVideo(String featuredVideo) {
this.featuredVideo = StringUtils.notNullStr(featuredVideo);
}
public String getBlogName() {
return StringUtils.notNullStr(blogName);
}
public void setBlogName(String blogName) {
this.blogName = StringUtils.notNullStr(blogName);
}
public String getBlogUrl() {
return StringUtils.notNullStr(blogUrl);
}
public void setBlogUrl(String blogUrl) {
this.blogUrl = StringUtils.notNullStr(blogUrl);
}
public String getPostAvatar() {
return StringUtils.notNullStr(postAvatar);
}
public void setPostAvatar(String postAvatar) {
this.postAvatar = StringUtils.notNullStr(postAvatar);
}
public String getPseudoId() {
return StringUtils.notNullStr(pseudoId);
}
public void setPseudoId(String pseudoId) {
this.pseudoId = StringUtils.notNullStr(pseudoId);
}
public String getDate() {
return StringUtils.notNullStr(date);
}
public void setDate(String dateStr) {
this.date = StringUtils.notNullStr(dateStr);
}
public String getPubDate() {
return StringUtils.notNullStr(pubDate);
}
public void setPubDate(String published) {
this.pubDate = StringUtils.notNullStr(published);
}
public String getPrimaryTag() {
return StringUtils.notNullStr(primaryTag);
}
public void setPrimaryTag(String tagName) {
// this is a bit of a hack to avoid setting the primary tag to one of the defaults
if (!ReaderTag.isDefaultTagTitle(tagName)) {
this.primaryTag = StringUtils.notNullStr(tagName);
}
}
boolean hasPrimaryTag() {
return !TextUtils.isEmpty(primaryTag);
}
public String getSecondaryTag() {
return StringUtils.notNullStr(secondaryTag);
}
public void setSecondaryTag(String tagName) {
if (!ReaderTag.isDefaultTagTitle(tagName)) {
this.secondaryTag = StringUtils.notNullStr(tagName);
}
}
/*
* attachments are stored as the actual JSON to avoid having a separate table for
* them, may need to revisit this if/when attachments become more important
*/
public String getAttachmentsJson() {
return StringUtils.notNullStr(attachmentsJson);
}
public void setAttachmentsJson(String json) {
attachmentsJson = StringUtils.notNullStr(json);
}
public boolean hasAttachments() {
return !TextUtils.isEmpty(attachmentsJson);
}
/*
* "discover" posts also store the actual JSON
*/
public String getDiscoverJson() {
return StringUtils.notNullStr(discoverJson);
}
public void setDiscoverJson(String json) {
discoverJson = StringUtils.notNullStr(json);
}
public boolean isDiscoverPost() {
return !TextUtils.isEmpty(discoverJson);
}
private transient ReaderPostDiscoverData discoverData;
public ReaderPostDiscoverData getDiscoverData() {
if (discoverData == null && !TextUtils.isEmpty(discoverJson)) {
try {
discoverData = new ReaderPostDiscoverData(new JSONObject(discoverJson));
} catch (JSONException e) {
return null;
}
}
return discoverData;
}
public boolean hasText() {
return !TextUtils.isEmpty(text);
}
public boolean hasUrl() {
return !TextUtils.isEmpty(url);
}
public boolean hasExcerpt() {
return !TextUtils.isEmpty(excerpt);
}
public boolean hasFeaturedImage() {
return !TextUtils.isEmpty(featuredImage);
}
public boolean hasFeaturedVideo() {
return !TextUtils.isEmpty(featuredVideo);
}
public boolean hasPostAvatar() {
return !TextUtils.isEmpty(postAvatar);
}
public boolean hasBlogName() {
return !TextUtils.isEmpty(blogName);
}
public boolean hasAuthorName() {
return !TextUtils.isEmpty(authorName);
}
public boolean hasAuthorFirstName() {
return !TextUtils.isEmpty(authorFirstName);
}
public boolean hasTitle() {
return !TextUtils.isEmpty(title);
}
public boolean hasBlogUrl() {
return !TextUtils.isEmpty(blogUrl);
}
/*
* returns true if this post is from a WordPress blog
*/
public boolean isWP() {
return !isExternal;
}
/*
* returns true if this is a cross-post
*/
public boolean isXpost() {
return xpostBlogId != 0 && xpostPostId != 0;
}
/*
* returns true if the passed post appears to be the same as this one - used when posts are
* retrieved to determine which ones are new/changed/unchanged
*/
public boolean isSamePost(ReaderPost post) {
return post != null
&& post.blogId == this.blogId
&& post.postId == this.postId
&& post.feedId == this.feedId
&& post.feedItemId == this.feedItemId
&& post.numLikes == this.numLikes
&& post.numReplies == this.numReplies
&& post.isFollowedByCurrentUser == this.isFollowedByCurrentUser
&& post.isLikedByCurrentUser == this.isLikedByCurrentUser
&& post.isCommentsOpen == this.isCommentsOpen
&& post.getTitle().equals(this.getTitle())
&& post.getExcerpt().equals(this.getExcerpt())
&& post.getText().equals(this.getText());
}
public boolean hasIds(ReaderBlogIdPostId ids) {
return ids != null
&& ids.getBlogId() == this.blogId
&& ids.getPostId() == this.postId;
}
/*
* liking is enabled for all wp.com and jp posts with the exception of discover posts
*/
public boolean canLikePost() {
return (isWP() || isJetpack) && (!isDiscoverPost());
}
public String getRailcarJson() {
return StringUtils.notNullStr(railcarJson);
}
public void setRailcarJson(String jsonRailcar) {
this.railcarJson = StringUtils.notNullStr(jsonRailcar);
}
public boolean hasRailcar() {
return !TextUtils.isEmpty(railcarJson);
}
/****
* the following are transient variables - not stored in the db or returned in the json - whose
* sole purpose is to cache commonly-used values for the post that speeds up using them inside
* adapters
****/
/*
* returns the featured image url as a photon url set to the passed width/height
*/
private transient String featuredImageForDisplay;
public String getFeaturedImageForDisplay(int width, int height) {
if (featuredImageForDisplay == null) {
if (!hasFeaturedImage()) {
featuredImageForDisplay = "";
} else {
featuredImageForDisplay = ReaderUtils.getResizedImageUrl(featuredImage, width, height, isPrivate);
}
}
return featuredImageForDisplay;
}
/*
* returns the avatar url as a photon url set to the passed size
*/
private transient String avatarForDisplay;
public String getPostAvatarForDisplay(int size) {
if (avatarForDisplay == null) {
if (!hasPostAvatar()) {
return "";
}
avatarForDisplay = GravatarUtils.fixGravatarUrl(postAvatar, size);
}
return avatarForDisplay;
}
/*
* returns the blog's blavatar url as a photon url set to the passed size
*/
private transient String blavatarForDisplay;
public String getPostBlavatarForDisplay(int size) {
if (blavatarForDisplay == null) {
if (!hasBlogUrl()) {
return "";
}
blavatarForDisplay = GravatarUtils.blavatarFromUrl(getBlogUrl(), size);
}
return blavatarForDisplay;
}
/*
* converts iso8601 pubDate to a java date for display - this is the date that appears on posts
*/
private transient java.util.Date dtDisplay;
public java.util.Date getDisplayDate() {
if (dtDisplay == null) {
dtDisplay = DateTimeUtils.dateFromIso8601(this.pubDate);
}
return dtDisplay;
}
/*
* determine which tag to display for this post
* - no tag if this is a private blog or there is no primary tag for this post
* - primary tag, unless it's the same as the currently selected tag
* - secondary tag if primary tag is the same as the currently selected tag
*/
private transient String tagForDisplay;
public String getTagForDisplay(final String currentTagName) {
if (tagForDisplay == null) {
if (!isPrivate && hasPrimaryTag()) {
if (getPrimaryTag().equalsIgnoreCase(currentTagName)) {
tagForDisplay = getSecondaryTag();
} else {
tagForDisplay = getPrimaryTag();
}
} else {
tagForDisplay = "";
}
}
return tagForDisplay;
}
/*
* used when a unique numeric id is required by an adapter (when hasStableIds() = true)
*/
private transient long stableId;
public long getStableId() {
if (stableId == 0) {
stableId = (pseudoId != null ? pseudoId.hashCode() : 0);
}
return stableId;
}
}