package kr.kdev.dg1s.biowiki.models;
import android.net.Uri;
import android.text.TextUtils;
import org.json.JSONObject;
import java.text.BreakIterator;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import kr.kdev.dg1s.biowiki.util.DateTimeUtils;
import kr.kdev.dg1s.biowiki.util.HtmlUtils;
import kr.kdev.dg1s.biowiki.util.JSONUtil;
import kr.kdev.dg1s.biowiki.util.PhotonUtils;
import kr.kdev.dg1s.biowiki.util.StringUtils;
import kr.kdev.dg1s.biowiki.util.UrlUtils;
/**
* Created by nbradbury on 6/27/13.
*/
public class ReaderPost {
public long postId;
public long blogId;
public long timestamp; // used for sorting
public int numReplies; // includes comments, trackbacks & pingbacks
public int numLikes;
public boolean isLikedByCurrentUser;
public boolean isFollowedByCurrentUser;
public boolean isRebloggedByCurrentUser;
public boolean isCommentsOpen;
public boolean isExternal;
public boolean isPrivate;
public boolean isVideoPress;
private String pseudoId;
private String title;
private String text;
private String excerpt;
private String authorName;
private String blogName;
private String blogUrl;
private String postAvatar;
private String tags; // comma-separated list of tags
private String published;
private String url;
private String featuredImage;
private String featuredVideo;
/**
* *
* 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;
/*
* returns the avatar url as a photon url set to the passed size
*/
private transient String avatarForDisplay;
/*
* converts iso8601 published date to an actual java date
*/
private transient java.util.Date dtPublished;
private transient String firstTag;
/*
* This is necessary to get VideoPress videos to work in the Reader since the v1
* REST API returns VideoPress videos in a script block that relies on jQuery - which obviously
* fails on mobile - here we extract the video thumbnail and insert a DIV at the top of the
* post content which links the thumbnail IMG to the video so the user can tap the thumb to
* play the video
* iOS: https://github.com/wordpress-mobile/WordPress-iOS/blob/develop/WordPress/Classes/ReaderPost.m#L702
*/
/*private static void cleanupVideoPress(ReaderPost post) {
if (post==null || !post.hasText() || !post.hasFeaturedVideo())
return;
// extract the video thumbnail from them "videopress-poster" image class
String text = post.getText();
int pos = text.indexOf("videopress-poster");
if (pos == -1)
return;
int srcStart = text.indexOf("src=\"", pos);
if (srcStart == -1)
return;
srcStart += 5;
int srcEnd = text.indexOf("\"", srcStart);
if (srcEnd == -1)
return;
// set the featured image to the thumbnail if a featured image isn't already assigned
String thumb = text.substring(srcStart, srcEnd);
if (!post.hasFeaturedImage())
post.featuredImage = thumb;
// add the thumbnail linked to the actual video to the top of the content
String videoLink = String.format("<div><a href='%s'><img src='%s'/></a></div>", post.getFeaturedVideo(), thumb);
post.text = videoLink + text;
}*/
// --------------------------------------------------------------------------------------------
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");
if (json.has("pseudo_ID")) {
post.pseudoId = JSONUtil.getString(json, "pseudo_ID"); // read/ endpoint
} else {
post.pseudoId = JSONUtil.getString(json, "global_ID"); // sites/ endpoint
}
// remove HTML from the excerpt
post.excerpt = HtmlUtils.fastStripHtml(JSONUtil.getString(json, "excerpt"));
post.text = JSONUtil.getString(json, "content");
post.title = JSONUtil.getStringDecoded(json, "title");
post.url = JSONUtil.getString(json, "URL");
post.blogUrl = JSONUtil.getString(json, "site_URL");
post.numReplies = json.optInt("comment_count");
post.numLikes = json.optInt("like_count");
post.isLikedByCurrentUser = JSONUtil.getBool(json, "i_like");
post.isFollowedByCurrentUser = JSONUtil.getBool(json, "is_following");
post.isRebloggedByCurrentUser = JSONUtil.getBool(json, "is_reblogged");
post.isCommentsOpen = JSONUtil.getBool(json, "comments_open");
post.isExternal = JSONUtil.getBool(json, "is_external");
post.isPrivate = JSONUtil.getBool(json, "site_is_private");
JSONObject jsonAuthor = json.optJSONObject("author");
if (jsonAuthor != null) {
post.authorName = JSONUtil.getString(jsonAuthor, "name");
post.postAvatar = JSONUtil.getString(jsonAuthor, "avatar_URL");
}
// only freshly-pressed posts have the "editorial" section
JSONObject jsonEditorial = json.optJSONObject("editorial");
if (jsonEditorial != null) {
post.blogId = jsonEditorial.optLong("blog_id");
post.blogName = JSONUtil.getStringDecoded(jsonEditorial, "blog_name");
post.featuredImage = getImageUrlFromFeaturedImageUrl(JSONUtil.getString(jsonEditorial, "image"));
// we want freshly-pressed posts to show & store the date they were chosen rather than the day they were published
post.published = JSONUtil.getString(jsonEditorial, "displayed_on");
} else {
post.featuredImage = JSONUtil.getString(json, "featured_image");
post.blogName = JSONUtil.getStringDecoded(json, "site_name");
post.published = JSONUtil.getString(json, "date");
}
// the date a post was liked is only returned by the read/liked/ endpoint - if this exists,
// set it as the timestamp so posts are sorted by the date they were liked rather than the
// date they were published (the timestamp is used to sort posts when querying)
String likeDate = JSONUtil.getString(json, "date_liked");
if (!TextUtils.isEmpty(likeDate)) {
post.timestamp = DateTimeUtils.iso8601ToTimestamp(likeDate);
} else {
post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published);
}
// parse attachments to get the VideoPress thumbnail & url
/*"attachments": {
"321": {
"ID": 321,
"URL": "http://dissolvingbuildings.files.wordpress.com/2013/08/webcam-video-from-25-august-2013-18-33.mp4",
"guid": "http://dissolvingbuildings.files.wordpress.com/2013/08/webcam-video-from-25-august-2013-18-33.mp4",
"mime_type": "video/mp4",
"width": 640,
"height": 360,
"duration": 21,
"videopress_files": {
"dvd": {
"url": "https://videos.files.wordpress.com/K2nkj5C2/webcam-video-from-25-august-2013-18-33_dvd.mp4",
"mime_type": "video/mp4"
},
"std": {
"url": "https://videos.files.wordpress.com/K2nkj5C2/webcam-video-from-25-august-2013-18-33_std.mp4",
"mime_type": "video/mp4"
},
"ogg": {
"url": "https://videos.files.wordpress.com/K2nkj5C2/webcam-video-from-25-august-2013-18-33_fmt1.ogv",
"mime_type": "video/ogg"
}
},
"videopress_thumbnail": "https://videos.files.wordpress.com/K2nkj5C2/webcam-video-from-25-august-2013-18-33_dvd.original.jpg"
}
}, */
JSONObject jsonAttachments = json.optJSONObject("attachments");
if (jsonAttachments != null) {
Iterator<String> it = jsonAttachments.keys();
if (it != null && it.hasNext()) {
JSONObject jsonFirstAttachment = jsonAttachments.optJSONObject(it.next());
if (jsonFirstAttachment != null) {
String thumbnail = JSONUtil.getString(jsonFirstAttachment, "videopress_thumbnail");
if (!TextUtils.isEmpty(thumbnail))
post.featuredImage = thumbnail;
JSONObject jsonVideoPress = jsonFirstAttachment.optJSONObject("videopress_files");
if (jsonVideoPress != null) {
JSONObject jsonStdVideo = jsonVideoPress.optJSONObject("std");
if (jsonStdVideo != null) {
post.featuredVideo = JSONUtil.getString(jsonStdVideo, "url");
post.isVideoPress = true;
}
}
}
}
}
// if there's no featured thumbnail, 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) {
String mediaUrl = JSONUtil.getString(jsonMedia, "uri");
if (!TextUtils.isEmpty(mediaUrl)) {
String type = JSONUtil.getString(jsonMedia, "type");
boolean isVideo = (type != null && type.equals("video"));
if (isVideo) {
post.featuredVideo = mediaUrl;
} else {
post.featuredImage = mediaUrl;
}
}
}
// if we still don't have a featured image, parse the content for an image that's
// suitable as a featured image - this is done since featured_media seems to miss
// some images that would work well as featured images on mobile
if (!post.hasFeaturedImage())
post.featuredImage = findFeaturedImage(post.text);
}
// if the post is untitled, make up a title from the excerpt
if (!post.hasTitle() && post.hasExcerpt())
post.title = extractTitle(post.excerpt, 50);
// extract comma-separated list of tags
JSONObject jsonTags = json.optJSONObject("tags");
if (jsonTags != null) {
StringBuilder sbTags = new StringBuilder();
Iterator<String> it = jsonTags.keys();
boolean isFirst = true;
while (it.hasNext()) {
if (isFirst) {
isFirst = false;
} else {
sbTags.append(",");
}
sbTags.append(it.next());
}
post.setTags(sbTags.toString());
}
// the single-post sites/$site/posts/$post endpoint doesn't return the blog_id/site_ID,
// instead all site metadata is returned under meta/data/site (assuming ?meta=site was
// added to the request) - check for this metadata if the blogId wasn't set above
if (post.blogId == 0) {
JSONObject jsonSite = JSONUtil.getJSONChild(json, "meta/data/site");
if (jsonSite != null) {
post.blogId = jsonSite.optInt("ID");
post.blogName = JSONUtil.getString(jsonSite, "name");
post.blogUrl = JSONUtil.getString(jsonSite, "URL");
post.isPrivate = JSONUtil.getBool(jsonSite, "is_private");
}
}
return post;
}
/*
* extracts a title from a post's excerpt
*/
private static String extractTitle(final String excerpt, int maxLen) {
if (TextUtils.isEmpty(excerpt))
return null;
if (excerpt.length() < maxLen)
return excerpt.trim();
//return excerpt.substring(0, maxLen).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() + "...";
}
/*
* called when a post doesn't have a featured image, searches post's content for an image that
* may still be suitable as a featured image - only works with WP posts due to the search for
* specific WP image classes (but will also work with RSS posts that come from WP blogs)
*/
private static String findFeaturedImage(final String text) {
if (text == null || !text.contains("<img "))
return null;
final String className;
if (text.contains("size-full")) {
className = "size-full";
} else if (text.contains("size-large")) {
className = "size-large";
} else if (text.contains("size-medium")) {
className = "size-medium";
} else {
return null;
}
// determine whether attributes are single- or double- quoted
boolean usesSingleQuotes = text.contains("src='");
int imgStart = text.indexOf("<img ");
while (imgStart > -1) {
int imgEnd = text.indexOf(">", imgStart);
if (imgEnd == -1)
return null;
String img = text.substring(imgStart, imgEnd + 1);
if (img.contains(className)) {
int srcStart = img.indexOf(usesSingleQuotes ? "src='" : "src=\"");
if (srcStart == -1)
return null;
int srcEnd = img.indexOf(usesSingleQuotes ? "'" : "\"", srcStart + 5);
if (srcEnd == -1)
return null;
return img.substring(srcStart + 5, srcEnd);
}
imgStart = text.indexOf("<img ", imgEnd);
}
// if we get this far, no suitable image was found
return null;
}
/*
returns the actual image url from a Freshly Pressed featured image url - this is necessary because the
featured image returned by the API is often an ImagePress url that formats the actual image url for a
specific size, and we want to define the size in the app when the image is requested.
here's an example of an ImagePress featured image url from a freshly-pressed post:
https://s1.wp.com/imgpress?crop=0px%2C0px%2C252px%2C160px&url=https%3A%2F%2Fs2.wp.com%2Fimgpress%3Fw%3D252%26url%3Dhttp%253A%252F%252Fmostlybrightideas.files.wordpress.com%252F2013%252F08%252Ftablet.png&unsharpmask=80,0.5,3
*/
private static String getImageUrlFromFeaturedImageUrl(final String featuredImageUrl) {
if (TextUtils.isEmpty(featuredImageUrl))
return null;
// if this is an mshots image, return the actual url without the query string (?h=n&w=n),
// and change it from https: to http: so it can be cached (it's only https because it's
// being returned by an authenticated REST endpoint - these images are found only in
// FP posts so they don't require https)
if (PhotonUtils.isMshotsUrl(featuredImageUrl))
return UrlUtils.removeQuery(featuredImageUrl).replaceFirst("https", "http");
if (featuredImageUrl.contains("imgpress")) {
// parse the url parameter
String actualImageUrl = Uri.parse(featuredImageUrl).getQueryParameter("url");
if (actualImageUrl == null)
return featuredImageUrl;
// at this point the imageUrl may still be an ImagePress url, so check the url param again (see above example)
if (actualImageUrl.contains("url=")) {
return Uri.parse(actualImageUrl).getQueryParameter("url");
} else {
return actualImageUrl;
}
}
// for all other featured images, return the passed url w/o the query string (since the query string
// often contains Photon sizing params that we don't want here)
int pos = featuredImageUrl.lastIndexOf("?");
if (pos == -1)
return featuredImageUrl;
return featuredImageUrl.substring(0, pos);
}
public String getAuthorName() {
return StringUtils.notNullStr(authorName);
}
public void setAuthorName(String authorName) {
this.authorName = StringUtils.notNullStr(authorName);
}
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);
}
public String getUrl() {
return StringUtils.notNullStr(url);
}
public void setUrl(String url) {
this.url = StringUtils.notNullStr(url);
}
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 getPublished() {
return StringUtils.notNullStr(published);
}
public void setPublished(String published) {
this.published = StringUtils.notNullStr(published);
}
// --------------------------------------------------------------------------------------------
/*
* comma-separated tags
*/
public String getTags() {
return StringUtils.notNullStr(tags);
}
public void setTags(String tags) {
this.tags = StringUtils.notNullStr(tags);
}
public boolean hasTags() {
return !TextUtils.isEmpty(tags);
}
List<String> getTagList() {
return Arrays.asList(getTags().split(","));
}
public boolean hasText() {
return !TextUtils.isEmpty(text);
}
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 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;
}
public String getFeaturedImageForDisplay(int width, int height) {
if (featuredImageForDisplay == null) {
if (!hasFeaturedImage())
return "";
if (isPrivate) {
// can't use photon on images in private posts since they require authentication, and must
// use https: in order for AuthToken to work when requesting them
featuredImageForDisplay = UrlUtils.makeHttps(featuredImage);
} else if (UrlUtils.isHttps(featuredImage)) {
// skip photon for https images since we can't authenticate them
featuredImageForDisplay = featuredImage;
} else {
// not private or https, so set to correctly sized photon url
featuredImageForDisplay = PhotonUtils.getPhotonImageUrl(featuredImage, width, height);
}
}
return featuredImageForDisplay;
}
public String getPostAvatarForDisplay(int avatarSize) {
if (avatarForDisplay == null) {
if (!hasPostAvatar())
return "";
avatarForDisplay = PhotonUtils.fixAvatar(postAvatar, avatarSize);
}
return avatarForDisplay;
}
public java.util.Date getDatePublished() {
if (dtPublished == null)
dtPublished = DateTimeUtils.iso8601ToJavaDate(published);
return dtPublished;
}
public String getFirstTag() {
if (firstTag == null) {
List<String> tags = getTagList();
if (tags != null && tags.size() > 0) {
firstTag = tags.get(0);
} else {
firstTag = "";
}
}
return firstTag;
}
}