package au.com.newint.newinternationalist;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamCorruptedException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.ListIterator;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import au.com.newint.newinternationalist.util.Purchase;
/**
* Created by New Internationalist on 4/02/15.
*/
public class Article implements Parcelable {
/* int id;
String title;
String teaser;
Date publication;
boolean keynote;
String featured_image_caption;
ArrayList featured_image;
ArrayList categories;
ArrayList images; */
JsonObject articleJson;
Issue parentIssue;
public Article(int id, Issue parentIssue) {
this(getArticleJsonForId(id, parentIssue), parentIssue);
}
public Article(JsonObject jsonObject, Issue parentIssue) {
this.articleJson = jsonObject;
this.parentIssue = parentIssue;
}
public Article(File jsonFile, Issue parentIssue) throws StreamCorruptedException {
this(Publisher.parseJsonFile(jsonFile).getAsJsonObject(),parentIssue);
}
public static JsonObject getArticleJsonForId(int id, Issue parentIssue) {
ArticleJsonCacheStreamFactory articleJsonCacheStreamFactory = new ArticleJsonCacheStreamFactory(id,parentIssue);
return (new JsonParser()).parse(new InputStreamReader(articleJsonCacheStreamFactory.createCacheInputStream())).getAsJsonObject();
}
public int getID() {
return articleJson.get("id").getAsInt();
}
public int getIssueID() {
return parentIssue.getID();
}
public String getTitle() {
return articleJson.get("title").getAsString();
}
public String getTeaser() {
return articleJson.get("teaser").getAsString();
}
public boolean isBodyOnFilesystem() {
return bodyCacheFile().exists();
}
private File bodyCacheFile() {
File articleDir = new File(Helpers.getStorageDirectory() + "/" + Integer.toString(getIssueID()) + "/", Integer.toString(getID()));
return new File(articleDir,"body.html");
}
private String getBody(ArrayList<Purchase> purchases) {
// POST request to rails.
URL articleBodyURL = null;
try {
articleBodyURL = new URL(Helpers.getSiteURL() + "issues/" + Integer.toString(getIssueID()) + "/articles/" + Integer.toString(getID()) + "/body_android");
} catch (MalformedURLException e) {
e.printStackTrace();
}
File cacheFile = bodyCacheFile();
String bodyHTML = null;
if (cacheFile.exists()) {
// Already have the body, so return it's contents as a string
// Helpers.debugLog("ArticleBody", "Filesystem hit! Returning from file.");
try {
FileInputStream inputStream = new FileInputStream(cacheFile);
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String receiveString = "";
StringBuilder stringBuilder = new StringBuilder();
while ( (receiveString = bufferedReader.readLine()) != null ) {
stringBuilder.append(receiveString);
}
inputStream.close();
bodyHTML = Helpers.wrapInHTML(stringBuilder.toString());
}
catch (FileNotFoundException e) {
Log.e("ArticleBody", "File not found: " + e.toString());
bodyHTML = Helpers.wrapInHTML("ERROR: File not found.");
} catch (IOException e) {
Log.e("ArticleBody", "Cannot read file: " + e.toString());
bodyHTML = Helpers.wrapInHTML("ERROR: Cannot read file.");
cacheFile.delete();
}
} else {
// Download the body
new DownloadBodyTask().execute(articleBodyURL, purchases);
}
return bodyHTML;
}
public String getExpandedBody(ArrayList<Purchase> purchases) {
// Expand [File:xxx] image tags
String articleBody = getBody(purchases);
if (articleBody != null) {
articleBody = expandImageTagsInBody(articleBody);
}
return articleBody;
}
private String expandImageTagsInBody(String body) {
ArrayList<Image> images = getImages();
Pattern regex = Pattern.compile("\\[File:(\\d+)(?:\\|([^\\]]*))?]");
Matcher regexMatcher = regex.matcher(body);
String imageID = null;
while (regexMatcher.find()) {
MatchResult matchResult = regexMatcher.toMatchResult();
String replacement = null;
Helpers.debugLog("ExpandedBody", "Group: " + regexMatcher.group());
Helpers.debugLog("ExpandedBody", "Group count: " + regexMatcher.groupCount());
Helpers.debugLog("ExpandedBody", "Group 1: " + regexMatcher.group(1));
imageID = regexMatcher.group(1);
String[] options = new String[0];
if (regexMatcher.group(2) != null) {
options = regexMatcher.group(2).split("\\|");
}
String cssClass = "article-image";
String imageWidth = "300";
for (String option : options) {
if (option.equalsIgnoreCase("full")) {
cssClass = "all-article-images article-image-cartoon article-image-full";
imageWidth = "945";
} else if (option.equalsIgnoreCase("cartoon")) {
cssClass = "all-article-images article-image-cartoon";
imageWidth = "600";
} else if (option.equalsIgnoreCase("centre")) {
cssClass = "all-article-images article-image-cartoon article-image-centre";
imageWidth = "300";
}
}
// Check for no shadow and left options
if (Arrays.asList(options).contains("ns")) {
cssClass += " no-shadow";
}
if (Arrays.asList(options).contains("left")) {
cssClass += " article-image-float-none";
}
if (Arrays.asList(options).contains("small")) {
cssClass += " article-image-small";
imageWidth = "150";
}
String credit_div = null;
String caption_div = null;
String imageCredit = null;
String imageCaption = null;
String imageSource = "file:///android_res/drawable/loading_image.png";
String imageZoomURI = "#";
Image image = null;
for (Image anImage : images) {
if (anImage.getID() == Integer.valueOf(imageID)) {
image = anImage;
}
}
if (image != null) {
imageCredit = image.getCredit();
imageCaption = image.getCaption();
}
if (imageCredit != null) {
credit_div = String.format("<div class='new-image-credit'>%1$s</div>", imageCredit);
}
if (imageCaption != null) {
caption_div = String.format("<div class='new-image-caption'>%1$s</div>", imageCaption);
}
replacement = String.format("<div class='%1$s'><a href='%2$s'><img id='image%3$s' width='%4$s' src='%5$s'/></a>%6$s%7$s</div>", cssClass, imageZoomURI, imageID, imageWidth, imageSource, caption_div, credit_div);
body = body.substring(0, matchResult.start()) + replacement + body.substring(matchResult.end());
regexMatcher.reset(body);
}
if (imageID == null && images != null && images.size() > 0) {
// There are images that haven't been embedded in the body
String imageStringOfMissingImages = "";
for (Image missedImage : images) {
// Build up new string of images
if (missedImage.getHidden()) {
// It's hidden, so don't show
} else {
imageStringOfMissingImages += String.format("[File:%1$s]", Integer.toString(missedImage.getID()));
}
}
// Add image [File: xx|full] tag to body and recursively try again
if (!imageStringOfMissingImages.equals("")) {
// Searching for article-body tag.. could do this better???
String emptyArticleBodyString = "<div class=\"article-body\">";
Pattern emptyBodyRegex = Pattern.compile(emptyArticleBodyString);
Matcher emptyBodyRegexMatcher = emptyBodyRegex.matcher(body);
boolean matchFound = false;
if (emptyBodyRegexMatcher.find()) {
MatchResult emptyMatchResult = emptyBodyRegexMatcher.toMatchResult();
body = body.substring(0, emptyMatchResult.start()) + body.substring(emptyMatchResult.start(), emptyMatchResult.end()) + imageStringOfMissingImages + body.substring(emptyMatchResult.end());
emptyBodyRegexMatcher.reset(body);
matchFound = true;
}
// Am I doing the recursion right here?
// TODO: Seems to be returning twice.. but works.
if (matchFound) {
return expandImageTagsInBody(body);
}
}
}
return body;
}
public Date getPublication() {
return Publisher.parseDateFromString(articleJson.get("publication").getAsString());
}
public boolean getKeynote() {
boolean keynote = false;
try {
keynote = articleJson.get("keynote").getAsBoolean();
} catch (Exception e) {
// Keynote is empty, so getAsBoolean barfs.
// TODO: more graceful fail?
// Helpers.debugLog("GetKeynote", e.toString());
}
return keynote;
}
public Article getNextArticle() {
Article nextArticle = null;
ArrayList<Article> articles = parentIssue.getArticles();
ListIterator<Article> articleListIterator = articles.listIterator();
for (int i = 0; i < articles.size(); i++) {
if (articles.get(i).getID() == this.getID() && i + 1 != articles.size()) {
nextArticle = articles.get(i + 1);
}
}
return nextArticle;
}
public String getFeaturedImageCaption() {
return articleJson.get("featured_image_caption").getAsString();
}
public ArrayList<Category> getCategories() {
JsonArray rootArray = articleJson.get("categories").getAsJsonArray();
ArrayList<Category> categories = new ArrayList<>();
if (rootArray != null) {
for (JsonElement aRootArray : rootArray) {
JsonObject jsonObject = aRootArray.getAsJsonObject();
if (jsonObject != null) {
Category category = new Category(jsonObject);
categories.add(category);
}
}
}
return categories;
}
public ArrayList<Image> getImages() {
JsonArray rootArray = articleJson.get("images").getAsJsonArray();
ArrayList<Image> images = new ArrayList<>();
if (rootArray != null) {
for (JsonElement aRootArray : rootArray) {
JsonObject jsonObject = aRootArray.getAsJsonObject();
if (jsonObject != null) {
Image image = new Image(jsonObject, parentIssue.getID(), this);
images.add(image);
}
}
}
// Sort the image array by position
Collections.sort(images, new Comparator<Image>() {
@Override
public int compare(Image lhs, Image rhs) {
return Double.compare(lhs.getPosition(), rhs.getPosition());
}
});
return images;
}
public URL getWebURL() {
try {
return new URL(Helpers.getSiteURL() + "issues/" + getIssueID() + "/articles/" + getID());
} catch (MalformedURLException e) {
return null;
}
}
public URLCacheStreamFactory getGuestPassURLCacheStreamFactory(ArrayList<Purchase> purchases) {
String username = Helpers.getFromPrefs(Helpers.LOGIN_USERNAME_KEY, "");
URL androidShareURL = null;
try {
androidShareURL = new URL(Helpers.getSiteURL() + "issues/" + getIssueID() + "/articles/" + getID() + "/android_share.json");
} catch (MalformedURLException e) {
Log.e("Article", "Can't build androidShareURL: " + e);
}
if (androidShareURL != null && (!username.equals("") || (purchases != null && purchases.size() > 0))) {
// Try and download the guestPass URL JSON from Rails for this user
HttpPost post = new HttpPost(androidShareURL.toString());
StringEntity stringEntity = purchasesToStringEntity(purchases);
if (stringEntity != null) {
post.setEntity(stringEntity);
}
return new URLCacheStreamFactory(post, null, null);
} else {
// The user doesn't have a login or any purchases, so just return the webURL
return null;
}
}
// PARCELABLE delegate methods
private Article(Parcel in) {
//int articleID = in.createIntArray()[0];
//int issueID = in.createIntArray()[1];
// strange behaviour occurs if we call createIntArray() twice,
// so we have yet-another-constructor that takes the int array
this(in.createIntArray());
}
// ... and then call the Article(articleID, parentIssue) constructor
private Article(int[] intArray) {
this(intArray[0], new Issue(intArray[1]));
}
public static final Parcelable.Creator<Article> CREATOR
= new Parcelable.Creator<Article>() {
public Article createFromParcel(Parcel in) {
return new Article(in);
}
public Article[] newArray(int size) {
return new Article[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
int[] intArray = {this.getID(), this.getIssueID()};
dest.writeIntArray(intArray);
}
// Download body async task
private class DownloadBodyTask extends AsyncTask<Object, Integer, ArrayList> {
@Override
protected ArrayList doInBackground(Object... objects) {
URL articleBodyURL = (URL) objects[0];
// Pass in purchases
ArrayList<Purchase> purchases = (ArrayList<Purchase>) objects[1];
String bodyHTML = "";
// Try logging into Rails for authentication.
DefaultHttpClient httpclient = new DefaultHttpClient();
// Setup post request
HttpContext ctx = new BasicHttpContext();
ctx.setAttribute(ClientContext.COOKIE_STORE, Publisher.INSTANCE.cookieStore);
HttpPost post = new HttpPost(articleBodyURL.toString());
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
// Add in-app purchase JSON data
if (purchases != null && purchases.size() > 0) {
StringEntity stringEntity = purchasesToStringEntity(purchases);
post.setEntity(stringEntity);
}
HttpResponse response = null;
try {
// Execute HTTP Post Request
response = httpclient.execute(post, ctx);
} catch (ClientProtocolException e) {
Helpers.debugLog("ArticleBody", "ClientProtocolException: " + e);
} catch (IOException e) {
Helpers.debugLog("ArticleBody", "IOException: " + e);
}
int responseStatusCode;
boolean success = false;
if (response != null) {
responseStatusCode = response.getStatusLine().getStatusCode();
if (responseStatusCode >= 200 && responseStatusCode < 300) {
// We have the article Body
success = true;
} else if (responseStatusCode > 400 && responseStatusCode < 500) {
// Article request failed
Helpers.debugLog("ArticleBody", "Failed with code: " + responseStatusCode);
} else {
// Server error.
Helpers.debugLog("ArticleBody", "Failed with code: " + responseStatusCode + " and response: " + response.getStatusLine());
}
} else {
// Error getting article body
Helpers.debugLog("ArticleBody", "Failed! Response is null");
}
if (success) {
try {
// Save to filesystem
bodyHTML = Helpers.wrapInHTML(EntityUtils.toString(response.getEntity(), "UTF-8"));
File dir = new File(Helpers.getStorageDirectory() + "/" + Integer.toString(getIssueID()) + "/", Integer.toString(getID()));
dir.mkdirs();
File file = new File(dir, "body.html");
try {
Writer w = new FileWriter(file);
w.write(bodyHTML);
w.close();
} catch (IOException e) {
e.printStackTrace();
Log.e("ArticleBody", "Error writing body to filesystem.");
}
} catch (IOException e) {
e.printStackTrace();
}
}
ArrayList<Object> responseList = new ArrayList<>();
responseList.add(response);
// Get expanded bodyHTML here too..
if (success) {
responseList.add(getExpandedBody(purchases));
}
return responseList;
}
@Override
protected void onPostExecute(ArrayList responseList) {
super.onPostExecute(responseList);
// Post body to listener
Publisher.ArticleBodyDownloadCompleteListener listener = Publisher.INSTANCE.articleBodyDownloadCompleteListener;
if (listener != null) {
listener.onArticleBodyDownloadComplete(responseList);
}
}
}
private StringEntity purchasesToStringEntity(ArrayList<Purchase> purchases) {
JsonArray purchasesJsonArray = new JsonArray();
for (Purchase purchase : purchases) {
// Send each purchase JSON data to rails to validate with Google Play
JsonParser parser = new JsonParser();
JsonObject purchaseJsonObject = (JsonObject)parser.parse(purchase.getOriginalJson());
purchasesJsonArray.add(purchaseJsonObject);
}
StringEntity stringEntity = null;
try {
stringEntity = new StringEntity(purchasesJsonArray.toString());
stringEntity.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return stringEntity;
}
}