com.nttec.everychan.ui.downloading.HtmlBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.nttec.everychan.ui.downloading.HtmlBuilder.java

Source

/*
 * Everychan Android (Meta Imageboard Client)
 * Copyright (C) 2014-2016  miku-nyan <https://github.com/miku-nyan>
 *     
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nttec.everychan.ui.downloading;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.nttec.everychan.R;
import com.nttec.everychan.api.ChanModule;
import com.nttec.everychan.api.models.AttachmentModel;
import com.nttec.everychan.api.models.BadgeIconModel;
import com.nttec.everychan.api.models.BoardModel;
import com.nttec.everychan.api.models.PostModel;
import com.nttec.everychan.api.models.ThreadModel;
import com.nttec.everychan.api.models.UrlPageModel;
import com.nttec.everychan.api.util.ChanModels;
import com.nttec.everychan.cache.SerializablePage;
import com.nttec.everychan.common.MainApplication;
import org.apache.commons.lang3.StringEscapeUtils;

import android.content.res.Resources;
import android.graphics.Color;
import android.text.Html;

/**
 * ? (??) HTML-?.<br>
 * ??  ?  ?,  ?  wakaba.<br> 
 * ? ?   ?? ? ? freedollchan.
 * @author miku-nyan
 *
 */
public class HtmlBuilder implements Closeable {

    /**   (css  js),   ??? -? */
    public static final String[] ASSETS = new String[] { "futaba.css", "photon.css", "burichan.css", "gurochan.css",
            "dollscript.js", "wakaba3.js" };

    /**        ? -?  ({@link #ASSETS}) */
    public static final String DATA_DIR = "data";

    private static final String DOLLSCRIPT = "dollscript.js";
    private static final String WAKABA3JS = "wakaba3.js";

    private static final String[] CSS = new String[] { "Futaba", "Photon", "Burichan", "Gurochan" };
    private static final String[] CSS_LINKS = new String[] { "futaba.css", "photon.css", "burichan.css",
            "gurochan.css" };

    private static final Pattern A_HREF_PATTERN = Pattern.compile("<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\"");

    private static final String CSS_FORMAT_1 = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" title=\"%s\" /> ";
    private static final String CSS_FORMAT_2 = "<link rel=\"alternate stylesheet\" type=\"text/css\" href=\"%s\" title=\"%s\" />";
    private static final String CSS_FORMAT_3 = "[<a href=\"javascript:set_stylesheet('%s')\">%s</a>] ";

    private static final String HTML_HEADER_1 = "<!DOCTYPE html>" + "<script type=\"text/javascript\" src=\"";
    private static final String HTML_HEADER_2 = "\"></script>" + "<html>" + "<head>"
            + "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />" + "<title>";
    private static final String HTML_HEADER_3 = "</title>" + "<link rel=\"icon\" type=\"image/png\" href=\"";
    private static final String HTML_HEADER_4 = "\" />" + "<style type=\"text/css\"> "
            + "body { margin: 0; padding: 8px; margin-bottom: auto; } "
            + "blockquote blockquote { margin-left: 0em } " + "form { margin-bottom: 0px } "
            + "form .trap { display:none } " + ".postarea { text-align: center } "
            + ".postarea table { margin: 0px auto; text-align: left } "
            + ".file { border: none; float: left; margin: 2px 20px } "
            + ".thumb { border: none; float: left; margin: 2px 20px } "
            + ".nothumb { float: left; background: #eee; border: 2px dashed #aaa; text-align: center; "
            + "margin: 2px 20px; padding: 1em 0.5em 1em 0.5em; } "
            + ".reply blockquote, blockquote :last-child { margin-bottom: 0em } "
            + ".reflink a { color: inherit; text-decoration: none } " + ".reply .filesize { margin-left: 20px } "
            + ".userdelete { float: right; text-align: center; white-space: nowrap } "
            + ".replypage .replylink { display: none } " + "</style>";
    private static final String HTML_HEADER_5 = "<script type=\"text/javascript\">var style_cookie=\"wakabastyle\";</script>"
            + "<script type=\"text/javascript\" src=\"";
    private static final String HTML_HEADER_6 = "\"></script>" + "</head>" + "<body class=\"replypage\">"
            + "<div class=\"adminbar\"> ";
    private static final String HTML_HEADER_7 = "</div>" + "<div class=\"logo\">";
    private static final String HTML_HEADER_8 = "</div>" + "<hr />"
            + "<form id=\"delform\" action=\"/wakaba/wakaba.pl\" method=\"post\">";

    private static final String HTML_FOOTER = "</form>" + "<p class=\"footer\"> - "
            + "<a href=\"http://miku-nyan.github.io/Everychan-Android/\">Everychan-android</a> + "
            + "<a href=\"http://wakaba.c3.cx/\">wakaba 3.0.9</a> + "
            + "<a href=\"http://www.2chan.net/\">futaba</a> + "
            + "<a href=\"http://www.1chan.net/futallaby/\">futallaby</a> -</p>" + "</body>" + "</html>";

    private final Writer buf;
    private final OutputStream _stream;
    private final boolean writeDeleted;
    private final RefsGetter refsGetter;
    private Resources res;
    private ChanModule chan;
    private UrlPageModel pageModel;
    private BoardModel boardModel;
    private DateFormat dateFormat;

    /**
     * ? ??
     * @param out ,    ? HTML
     * @param refsGetter ? ? ? ??  ?  
     */
    public HtmlBuilder(OutputStream out, RefsGetter refsGetter) throws IOException {
        this(out, true, refsGetter);
    }

    /**
     * ? ??
     * @param out ,    ? HTML
     * @param writeDeleted ?  ?
     * @param refsGetter ? ? ? ??  ?  
     */
    public HtmlBuilder(OutputStream out, boolean writeDeleted, RefsGetter refsGetter) throws IOException {
        _stream = out;
        buf = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));

        this.writeDeleted = writeDeleted;
        this.refsGetter = refsGetter;
    }

    public void write(SerializablePage page) throws IOException {
        this.res = MainApplication.getInstance().resources;
        this.chan = MainApplication.getInstance().getChanModule(page.boardModel.chan);
        this.pageModel = page.pageModel;
        this.boardModel = page.boardModel;
        this.dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
        this.dateFormat.setTimeZone(TimeZone.getTimeZone(boardModel.timeZoneId));

        String logo = (page.boardModel.boardDescription != null ? page.boardModel.boardDescription
                : page.boardModel.boardName);
        try {
            String url = chan.buildUrl(pageModel);
            logo += " <a href=\"" + url + "\">(" + chan.getChanName() + ")</a>";
        } catch (Exception e) {
            /* ignore */ }

        buildHeader(buildTitle(page), logo);

        if (page.posts != null && page.posts.length != 0) {
            ThreadModel thread = new ThreadModel();
            thread.posts = page.posts;
            thread.threadNumber = page.posts[0].number;
            thread.postsCount = -1;
            thread.attachmentsCount = -1;
            buildThread(thread);
        }
        if (page.threads != null) {
            for (ThreadModel thread : page.threads)
                buildThread(thread);
        }
        buf.write(HTML_FOOTER);
        buf.flush();
    }

    public static String buildTitle(SerializablePage page) {
        String title;
        if (page.posts != null && page.posts.length != 0) {
            title = "/" + page.boardModel.boardName + " - ";
            title += page.posts[0].subject != null && page.posts[0].subject.length() != 0 ? page.posts[0].subject
                    : Html.fromHtml(page.posts[0].comment).toString().replace('\n', ' ');
            if (title.length() > 255)
                title = title.substring(0, 256);
        } else {
            title = page.boardModel.boardDescription != null ? page.boardModel.boardDescription
                    : page.boardModel.boardName;
        }
        return title;
    }

    @Override
    public void close() throws IOException {
        try {
            buf.close();
        } catch (IOException e) {
            _stream.close();
            throw e;
        }
    }

    private void buildHeader(String pageTitle, String logoTitle) throws IOException {
        buf.write(HTML_HEADER_1);
        buf.write(DATA_DIR + "/" + DOLLSCRIPT);
        buf.write(HTML_HEADER_2);
        buf.write(pageTitle);
        buf.write(HTML_HEADER_3);
        buf.write(refsGetter.getFavicon());
        buf.write(HTML_HEADER_4);
        buf.write(String.format(Locale.US, CSS_FORMAT_1, (DATA_DIR + "/" + CSS_LINKS[0]), CSS[0]));
        for (int i = 1; i < CSS.length; ++i)
            buf.write(String.format(Locale.US, CSS_FORMAT_2, (DATA_DIR + "/" + CSS_LINKS[i]), CSS[i]));
        buf.write(HTML_HEADER_5);
        buf.write(DATA_DIR + "/" + WAKABA3JS);
        buf.write(HTML_HEADER_6);
        for (int i = 0; i < CSS.length; ++i)
            buf.write(String.format(Locale.US, CSS_FORMAT_3, CSS[i], CSS[i]));
        buf.write(HTML_HEADER_7);
        buf.write(logoTitle);
        buf.write(HTML_HEADER_8);
    }

    private void buildThread(ThreadModel thread) throws IOException {
        PostModel[] posts = thread.posts;
        if (posts == null || posts.length == 0)
            return;
        buildPost(posts[0], true);
        for (int i = 1; i < posts.length; ++i)
            buildPost(posts[i], false);
        closeThread();
    }

    private void closeThread() throws IOException {
        buf.write("<br clear=\"left\" /><hr /> ");
    }

    private void buildPost(PostModel model, boolean isOpPost) throws IOException {
        if (!isOpPost && !writeDeleted && model.deleted)
            return;
        if (!isOpPost) {
            buf.write("<table><tbody><tr><td class=\"doubledash\">&gt;&gt;</td> <td class=\"reply\" id=\"reply");
            buf.write(model.number);
            buf.write("\"> ");
        }
        buf.write("<a name=\"");
        buf.write(model.number);
        buf.write("\"></a> <label><input type=\"checkbox\" name=\"delete\" value=\"");
        buf.write(model.number);
        buf.write("\" /> <span class=\"");
        buf.write(isOpPost ? "filetitle" : "replytitle");
        buf.write("\">");
        if (model.subject != null)
            buf.write(StringEscapeUtils.escapeHtml4(model.subject));
        buf.write("</span> <span class=\"");
        if (!isOpPost)
            buf.write("comment");
        buf.write("postername\">");
        if (model.color != Color.TRANSPARENT) {
            buf.write("<font color=\"");
            buf.write(String.format("#%06X", (0xFFFFFF & model.color)));
            buf.write("\">&#9632;</font>");
        }
        String name = StringEscapeUtils.escapeHtml4(model.name == null ? model.email : model.name);
        if (name != null) {
            if (model.email != null && model.email.length() != 0) {
                buf.write("<a href=\"");
                if (!model.email.contains(":"))
                    buf.write("mailto:");
                buf.write(model.email);
                buf.write("\">");
                buf.write(name);
                buf.write("</a>");
            } else
                buf.write(name);
        }
        buf.write("</span> ");
        if (model.icons != null) {
            boolean firstIcon = true;
            for (BadgeIconModel icon : model.icons) {
                if (!firstIcon)
                    buf.write("&nbsp;");
                firstIcon = false;
                buf.write("<img hspace=\"3\" src=\"");
                buf.write(refsGetter.getIcon(icon));
                buf.write("\" title=\"");
                buf.write((icon.description != null && icon.description.length() != 0) ? icon.description
                        : (icon.source == null ? "" : icon.source.substring(icon.source.lastIndexOf('/') + 1)));
                buf.write("\" border=\"0\" />");
            }
            buf.write(' ');
        }
        if (model.trip != null && model.trip.length() != 0) {
            buf.write("<span class=\"postertrip\">");
            buf.write(StringEscapeUtils.escapeHtml4(model.trip));
            buf.write("</span> ");
        }
        if (model.op)
            buf.write("<span class=\"opmark\"># OP</span> ");
        buf.write(StringEscapeUtils.escapeHtml4(dateFormat.format(model.timestamp)));
        buf.write("</label> <span class=\"reflink\">  <a href=\"javascript:insert('&gt;&gt;");
        buf.write(model.number);
        buf.write("')\">No.");
        buf.write(model.number);
        buf.write("</a> </span>");
        if (model.deleted)
            buf.write("<span class=\"de-post-deleted\"></span>");
        buf.write("&nbsp; ");
        if (model.attachments != null && model.attachments.length != 0) {
            buf.write("<br />");
            boolean single = model.attachments.length == 1;
            for (AttachmentModel attachment : model.attachments)
                buildAttachment(attachment, single);
            if (!single)
                buf.write("<br clear=\"left\" />");
        }
        buf.write("<blockquote>");
        buf.write(fixComment(model.comment));
        buf.write("</blockquote>");
        if (!isOpPost) {
            buf.write("</td></tr></tbody></table>");
        }
    }

    private String fixComment(String comment) {
        comment = comment.replaceAll("(?i)<aibquote>", "<span class=\"unkfunc\">")
                .replaceAll("(?i)</aibquote>", "</span>").replaceAll("(?i)<aibspoiler>", "<span class=\"spoiler\">")
                .replaceAll("(?i)</aibspoiler>", "</span>");
        Matcher m = A_HREF_PATTERN.matcher(comment);
        if (!m.find())
            return comment;
        StringBuffer sb = new StringBuffer();
        do {
            String group = m.group();
            String found = m.group(1);
            int oldPos = m.start(1) - m.start();
            int oldLen = found.length();

            String url;
            if (found.startsWith("#")) {
                try {
                    String thisThreadUrl = chan.buildUrl(pageModel);
                    int i = thisThreadUrl.indexOf('#');
                    if (i != -1)
                        thisThreadUrl = thisThreadUrl.substring(0, i);
                    String postNumber = chan.parseUrl(thisThreadUrl + found).postNumber;
                    url = "#" + postNumber != null ? postNumber : pageModel.threadNumber;
                } catch (Exception e) {
                    url = found;
                }
            } else {
                url = chan.fixRelativeUrl(found);
                try {
                    UrlPageModel linkModel = chan.parseUrl(url);
                    if (ChanModels.hashUrlPageModel(linkModel).equals(ChanModels.hashUrlPageModel(pageModel))) {
                        url = "#" + linkModel.postNumber;
                    }
                } catch (Exception e) {
                    /* ignore */ }
            }

            m.appendReplacement(sb, url.equals(found) ? group
                    : (group.substring(0, oldPos) + url + group.substring(oldPos + oldLen)));
        } while (m.find());
        m.appendTail(sb);
        return sb.toString();
    }

    private void buildAttachment(AttachmentModel model, boolean isSingle) throws IOException {
        int tnWidth, tnHeight;
        if (model.width > 0 && model.height > 0) {
            float scale = 200f / Math.max(model.width, model.height);
            if (scale > 1)
                scale = 1;
            tnWidth = (int) (scale * model.width);
            tnHeight = (int) (scale * model.height);
        } else {
            tnWidth = -1;
            tnHeight = -1;
        }

        String thumbRef = refsGetter.getThumbnail(model);
        String origRef = refsGetter.getOriginal(model);

        String filenameDesc;
        if (model.type != AttachmentModel.TYPE_OTHER_NOTFILE) {
            filenameDesc = model.path != null ? model.path : model.thumbnail;
            filenameDesc = filenameDesc.substring(filenameDesc.lastIndexOf('/') + 1);
            try {
                filenameDesc = URLDecoder.decode(filenameDesc, "UTF-8");
            } catch (Exception e) {
                /*ignore*/}
        } else {
            filenameDesc = res.getString(R.string.html_external);
        }

        if (!isSingle)
            buf.write("<div class=\"file\">");
        buf.write("<span class=\"filesize\">");
        if (model.type != AttachmentModel.TYPE_OTHER_NOTFILE)
            buf.write(res.getString(R.string.html_file));
        buf.write(" <a target=\"_blank\" href=\"");
        buf.write(origRef);
        buf.write("\">");
        buf.write(filenameDesc);
        buf.write("</a>");
        if (model.type != AttachmentModel.TYPE_OTHER_NOTFILE) {
            buf.write(isSingle ? " - " : "<br />");
            buf.write("(<em>");
            boolean first = true;
            if (model.size != -1) {
                first = false;
                buf.write(String.format(Locale.US, "%d KB", model.size));
            }
            if (model.width > 0 && model.height > 0) {
                if (!first)
                    buf.write(", ");
                else
                    first = false;
                buf.write(String.format(Locale.US, "%dx%d", model.width, model.height));
            }
            if (model.originalName != null && model.originalName.length() > 0) {
                if (!first)
                    buf.write(", ");
                else
                    first = false;
                buf.write(model.originalName);
            }
            buf.write("</em>)");
        }
        buf.write("</span> ");
        if (thumbRef != null) {
            if (isSingle) {
                buf.write(" <span class=\"thumbnailmsg\">");
                buf.write(res.getString(R.string.html_thumbnailmsg));
                buf.write("</span>");
            }
            buf.write("<br /><a target=\"_blank\" href=\"");
            buf.write(origRef);
            buf.write("\"> <img src=\"");
            buf.write(thumbRef);

            if (tnWidth == -1) {
                buf.write(
                        "\" onload=\"with (this) {if (offsetHeight > offsetWidth) style.height = '200px'; else style.width = '200px'}\"");
            } else {
                buf.write(String.format(Locale.US, "\" width=\"%d\" height=\"%d\"", tnWidth, tnHeight));
            }
            buf.write(String.format(Locale.US, " alt=\"%s\" ", filenameDesc));

            if (isSingle)
                buf.write("class=\"thumb\" ");
            buf.write("/></a>");
        }
        if (!isSingle)
            buf.write("</div>");
    }

    public static interface RefsGetter {
        /**  ?  favicon ( ) */
        String getFavicon();

        /**  ?  ? (     ,    URL) */
        String getOriginal(AttachmentModel attachment);

        /**     ? (   null) */
        String getThumbnail(AttachmentModel attachment);

        /**  ?  ?  ( ,    null) */
        String getIcon(BadgeIconModel icon);
    }

}