/*
* Overchan 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 nya.miku.wishmaster.api;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.tuple.Pair;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.HttpHeaders;
import cz.msebera.android.httpclient.NameValuePair;
import cz.msebera.android.httpclient.client.HttpClient;
import cz.msebera.android.httpclient.client.entity.UrlEncodedFormEntity;
import cz.msebera.android.httpclient.entity.mime.content.ByteArrayBody;
import cz.msebera.android.httpclient.message.BasicHeader;
import cz.msebera.android.httpclient.message.BasicNameValuePair;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.interfaces.ProgressListener;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.BadgeIconModel;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.api.models.DeletePostModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.SendPostModel;
import nya.miku.wishmaster.api.models.ThreadModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.api.util.CryptoUtils;
import nya.miku.wishmaster.api.util.RegexUtils;
import nya.miku.wishmaster.api.util.UrlPathUtils;
import nya.miku.wishmaster.api.util.WakabaUtils;
import nya.miku.wishmaster.common.IOUtils;
import nya.miku.wishmaster.http.ExtendedMultipartBuilder;
import nya.miku.wishmaster.http.streamer.HttpRequestModel;
import nya.miku.wishmaster.http.streamer.HttpResponseModel;
import nya.miku.wishmaster.http.streamer.HttpStreamer;
import nya.miku.wishmaster.http.streamer.HttpWrongStatusCodeException;
import nya.miku.wishmaster.lib.org_json.JSONArray;
import nya.miku.wishmaster.lib.org_json.JSONObject;
import android.content.SharedPreferences;
import android.content.res.Resources;
public abstract class AbstractVichanModule extends AbstractWakabaModule {
private static final Pattern ATTACHMENT_EMBEDDED_LINK = Pattern.compile("<a[^>]*href=\"([^\">]*)\"[^>]*>");
private static final Pattern ATTACHMENT_EMBEDDED_THUMB = Pattern.compile("<img[^>]*src=\"([^\">]*)\"[^>]*>");
private static final Pattern ERROR_PATTERN = Pattern.compile("<h2 [^>]*>(.*?)</h2>");
public AbstractVichanModule(SharedPreferences preferences, Resources resources) {
super(preferences, resources);
}
@Override
public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception {
BoardModel board = super.getBoard(shortName, listener, task);
board.timeZoneId = "UTC";
board.defaultUserName = "Anonymous";
board.readonlyBoard = false;
board.requiredFileForNewThread = true;
board.allowDeletePosts = true;
board.allowDeleteFiles = true;
board.allowReport = BoardModel.REPORT_WITH_COMMENT;
board.allowNames = true;
board.allowSubjects = true;
board.allowSage = true;
board.allowEmails = true;
board.ignoreEmailIfSage = true;
board.allowCustomMark = false;
board.allowRandomHash = true;
board.allowIcons = false;
board.attachmentsMaxCount = 1;
board.attachmentsFormatFilters = null;
board.markType = BoardModel.MARK_BBCODE;
board.firstPage = 1;
board.catalogAllowed = true;
return board;
}
@Override
public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task, ThreadModel[] oldList)
throws Exception {
String url = getUsingUrl() + boardName + "/" + (page-1) + ".json";
JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
if (response == null) return oldList;
JSONArray threads = response.getJSONArray("threads");
ThreadModel[] result = new ThreadModel[threads.length()];
for (int i=0, len=threads.length(); i<len; ++i) {
JSONArray posts = threads.getJSONObject(i).getJSONArray("posts");
JSONObject op = posts.getJSONObject(0);
ThreadModel curThread = mapThreadModel(op, boardName);
curThread.posts = new PostModel[posts.length()];
for (int j=0, plen=posts.length(); j<plen; ++j) {
curThread.posts[j] = mapPostModel(posts.getJSONObject(j), boardName);
}
result[i] = curThread;
}
return result;
}
@Override
public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList)
throws Exception {
String url = getUsingUrl() + boardName + "/res/" + threadNumber + ".json";
JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
if (response == null) return oldList;
JSONArray posts = response.getJSONArray("posts");
PostModel[] result = new PostModel[posts.length()];
for (int i=0, len=posts.length(); i<len; ++i) {
result[i] = mapPostModel(posts.getJSONObject(i), boardName);
}
if (oldList != null) {
result = ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(result));
}
return result;
}
@Override
public ThreadModel[] getCatalog(String boardName, int catalogType, ProgressListener listener, CancellableTask task, ThreadModel[] oldList) throws Exception {
String url = getUsingUrl() + boardName + "/catalog.json";
JSONArray response = downloadJSONArray(url, oldList != null, listener, task);
if (response == null) return oldList;
List<ThreadModel> threads = new ArrayList<>();
for (int i=0, len=response.length(); i<len; ++i) {
JSONArray curArray = response.getJSONObject(i).getJSONArray("threads");
for (int j=0, clen=curArray.length(); j<clen; ++j) {
JSONObject curThreadJson = curArray.getJSONObject(j);
ThreadModel curThread = new ThreadModel();
curThread.threadNumber = Long.toString(curThreadJson.getLong("no"));
curThread.postsCount = curThreadJson.optInt("replies", -2) + 1;
curThread.attachmentsCount = curThreadJson.optInt("images", -2) + 1;
curThread.isSticky = curThreadJson.optInt("sticky") == 1;
curThread.isClosed = curThreadJson.optInt("closed") == 1;
curThread.posts = new PostModel[] { mapPostModel(curThreadJson, boardName) };
threads.add(curThread);
}
}
return threads.toArray(new ThreadModel[threads.size()]);
}
protected ThreadModel mapThreadModel(JSONObject opPost, String boardName) {
ThreadModel curThread = new ThreadModel();
curThread.threadNumber = Long.toString(opPost.getLong("no"));
curThread.postsCount = opPost.optInt("replies", -2) + 1;
curThread.attachmentsCount = opPost.optInt("images", -2) + 1;
if (curThread.attachmentsCount >= 0) curThread.attachmentsCount += opPost.optInt("omitted_images", 0);
curThread.isSticky = opPost.optInt("sticky") == 1;
curThread.isClosed = opPost.optInt("closed") == 1;
return curThread;
}
protected PostModel mapPostModel(JSONObject object, String boardName) {
PostModel model = new PostModel();
model.number = Long.toString(object.getLong("no"));
model.name = StringEscapeUtils.unescapeHtml4(RegexUtils.removeHtmlSpanTags(object.optString("name", "Anonymous")));
model.subject = StringEscapeUtils.unescapeHtml4(object.optString("sub", ""));
model.comment = object.optString("com", "");
model.email = object.optString("email", "");
model.trip = object.optString("trip", "");
String capcode = object.optString("capcode", "none");
if (!capcode.equals("none")) model.trip += "##"+capcode;
String countryIcon = object.optString("country", "");
if (!countryIcon.equals("")) {
BadgeIconModel icon = new BadgeIconModel();
icon.source = "/static/flags/" + countryIcon.toLowerCase(Locale.US) + ".png";
icon.description = object.optString("country_name");
model.icons = new BadgeIconModel[] { icon };
}
model.op = false;
String id = object.optString("id", "");
model.sage = id.equalsIgnoreCase("Heaven") || model.email.toLowerCase(Locale.US).contains("sage");
if (!id.equals("")) model.name += (" ID:" + id);
if (!id.equals("") && !id.equalsIgnoreCase("Heaven")) model.color = CryptoUtils.hashIdColor(id);
model.timestamp = object.getLong("time") * 1000;
model.parentThread = object.optString("resto", "0");
if (model.parentThread.equals("0")) model.parentThread = model.number;
List<AttachmentModel> attachments = null;
boolean isSpoiler = object.optInt("spoiler") == 1;
AttachmentModel rootAttachment = mapAttachment(object, boardName, isSpoiler);
if (rootAttachment != null) {
attachments = new ArrayList<>();
attachments.add(rootAttachment);
JSONArray extraFiles = object.optJSONArray("extra_files");
if (extraFiles != null && extraFiles.length() != 0) {
for (int i=0, len=extraFiles.length(); i<len; ++i) {
AttachmentModel attachment = mapAttachment(extraFiles.getJSONObject(i), boardName, isSpoiler);
if (attachment != null) attachments.add(attachment);
}
}
}
String embed = object.optString("embed", "");
if (!embed.equals("")) {
AttachmentModel embedAttachment = new AttachmentModel();
embedAttachment.type = AttachmentModel.TYPE_OTHER_NOTFILE;
Matcher linkMatcher = ATTACHMENT_EMBEDDED_LINK.matcher(embed);
if (linkMatcher.find()) {
embedAttachment.path = linkMatcher.group(1);
if (embedAttachment.path.startsWith("//")) embedAttachment.path = (useHttps() ? "https:" : "http:") + embedAttachment.path;
Matcher thumbMatcher = ATTACHMENT_EMBEDDED_THUMB.matcher(embed);
if (thumbMatcher.find()) {
embedAttachment.thumbnail = thumbMatcher.group(1);
if (embedAttachment.thumbnail.startsWith("//")) embedAttachment.thumbnail = (useHttps() ? "https:" : "http:") + embedAttachment.thumbnail;
}
embedAttachment.isSpoiler = isSpoiler;
embedAttachment.size = -1;
if (attachments != null) attachments.add(embedAttachment); else attachments = Collections.singletonList(embedAttachment);
}
}
if (attachments != null) model.attachments = attachments.toArray(new AttachmentModel[attachments.size()]);
return model;
}
protected AttachmentModel mapAttachment(JSONObject object, String boardName, boolean isSpoiler) {
String ext = object.optString("ext", "");
if (!ext.equals("")) {
AttachmentModel attachment = new AttachmentModel();
switch (ext) {
case ".jpeg":
case ".jpg":
case ".png":
attachment.type = AttachmentModel.TYPE_IMAGE_STATIC;
break;
case ".gif":
attachment.type = AttachmentModel.TYPE_IMAGE_GIF;
break;
case ".svg":
case ".svgz":
attachment.type = AttachmentModel.TYPE_IMAGE_SVG;
break;
case ".mp3":
case ".ogg":
attachment.type = AttachmentModel.TYPE_AUDIO;
break;
case ".webm":
case ".mp4":
attachment.type = AttachmentModel.TYPE_VIDEO;
break;
default:
attachment.type = AttachmentModel.TYPE_OTHER_FILE;
}
attachment.size = object.optInt("fsize", -1);
if (attachment.size > 0) attachment.size = Math.round(attachment.size / 1024f);
attachment.width = object.optInt("w", -1);
attachment.height = object.optInt("h", -1);
attachment.originalName = object.optString("filename", "") + ext;
attachment.isSpoiler = isSpoiler;
String tim = object.optString("tim", "");
if (tim.length() > 0) {
attachment.thumbnail = isSpoiler || attachment.type == AttachmentModel.TYPE_AUDIO ? null :
("/" + boardName + "/thumb/" + tim + ".jpg");
attachment.path = "/" + boardName + "/src/" + tim + ext;
return attachment;
}
}
return null;
}
@Override
public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception {
UrlPageModel urlModel = new UrlPageModel();
urlModel.chanName = getChanName();
urlModel.boardName = model.boardName;
if (model.threadNumber == null) {
urlModel.type = UrlPageModel.TYPE_BOARDPAGE;
urlModel.boardPage = UrlPageModel.DEFAULT_FIRST_PAGE;
} else {
urlModel.type = UrlPageModel.TYPE_THREADPAGE;
urlModel.threadNumber = model.threadNumber;
}
String referer = buildUrl(urlModel);
List<Pair<String, String>> fields = VichanAntiBot.getFormValues(referer, task, httpClient);
if (task != null && task.isCancelled()) throw new Exception("interrupted");
ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().
setCharset(Charset.forName("UTF-8")).setDelegates(listener, task);
for (Pair<String, String> pair : fields) {
if (pair.getKey().equals("spoiler") && !model.custommark) continue;
String val;
switch (pair.getKey()) {
case "name": val = model.name; break;
case "email": val = getSendPostEmail(model); break;
case "subject": val = model.subject; break;
case "body": val = model.comment; break;
case "password": val = model.password; break;
case "spoiler": val = "on"; break;
default: val = pair.getValue();
}
if (pair.getKey().equals("file")) {
if (model.attachments != null && model.attachments.length > 0) {
postEntityBuilder.addFile(pair.getKey(), model.attachments[0], model.randomHash);
} else {
postEntityBuilder.addPart(pair.getKey(), new ByteArrayBody(new byte[0], ""));
}
} else {
postEntityBuilder.addString(pair.getKey(), val);
}
}
String url = getUsingUrl() + "post.php";
Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, referer) };
HttpRequestModel request =
HttpRequestModel.builder().setPOST(postEntityBuilder.build()).setCustomHeaders(customHeaders).setNoRedirect(true).build();
HttpResponseModel response = null;
try {
response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, listener, task);
if (response.statusCode == 200 || response.statusCode == 400) {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
IOUtils.copyStream(response.stream, output);
String htmlResponse = output.toString("UTF-8");
Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
if (errorMatcher.find()) throw new Exception(errorMatcher.group(1));
} else if (response.statusCode == 303) {
for (Header header : response.headers) {
if (header != null && HttpHeaders.LOCATION.equalsIgnoreCase(header.getName())) {
return fixRelativeUrl(header.getValue());
}
}
}
throw new Exception(response.statusCode + " - " + response.statusReason);
} finally {
if (response != null) response.release();
}
}
@Override
public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = getUsingUrl() + "post.php";
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
pairs.add(new BasicNameValuePair("board", model.boardName));
pairs.add(new BasicNameValuePair("delete_" + model.postNumber, "on"));
if (model.onlyFiles) pairs.add(new BasicNameValuePair("file", "on"));
pairs.add(new BasicNameValuePair("password", model.password));
pairs.add(new BasicNameValuePair("delete", getDeleteFormValue(model)));
pairs.add(new BasicNameValuePair("reason", ""));
UrlPageModel refererPage = new UrlPageModel();
refererPage.type = UrlPageModel.TYPE_THREADPAGE;
refererPage.chanName = getChanName();
refererPage.boardName = model.boardName;
refererPage.threadNumber = model.threadNumber;
Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, buildUrl(refererPage)) };
HttpRequestModel rqModel = HttpRequestModel.builder().
setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setCustomHeaders(customHeaders).setNoRedirect(true).build();
HttpResponseModel response = null;
try {
response = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, listener, task);
if (response.statusCode == 200 || response.statusCode == 400 || response.statusCode == 303) {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
IOUtils.copyStream(response.stream, output);
String htmlResponse = output.toString("UTF-8");
Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
if (errorMatcher.find()) throw new Exception(errorMatcher.group(1));
return null;
}
throw new Exception(response.statusCode + " - " + response.statusReason);
} finally {
if (response != null) response.release();
}
}
@Override
public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = getUsingUrl() + "post.php";
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
pairs.add(new BasicNameValuePair("board", model.boardName));
pairs.add(new BasicNameValuePair("delete_" + model.postNumber, "on"));
pairs.add(new BasicNameValuePair("password", ""));
pairs.add(new BasicNameValuePair("reason", model.reportReason));
pairs.add(new BasicNameValuePair("report", getReportFormValue(model)));
UrlPageModel refererPage = new UrlPageModel();
refererPage.type = UrlPageModel.TYPE_THREADPAGE;
refererPage.chanName = getChanName();
refererPage.boardName = model.boardName;
refererPage.threadNumber = model.threadNumber;
Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, buildUrl(refererPage)) };
HttpRequestModel rqModel = HttpRequestModel.builder().
setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setCustomHeaders(customHeaders).setNoRedirect(true).build();
HttpResponseModel response = null;
try {
response = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, listener, task);
if (response.statusCode == 200 || response.statusCode == 400 || response.statusCode == 303) {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
IOUtils.copyStream(response.stream, output);
String htmlResponse = output.toString("UTF-8");
Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
if (errorMatcher.find()) throw new Exception(errorMatcher.group(1));
return null;
}
throw new Exception(response.statusCode + " - " + response.statusReason);
} finally {
if (response != null) response.release();
}
}
protected String getSendPostEmail(SendPostModel model) {
return model.sage ? "sage" : model.email;
}
protected String getDeleteFormValue(DeletePostModel model) {
return "Delete";
}
protected String getReportFormValue(DeletePostModel model) {
return "Report";
}
@Override
public String buildUrl(UrlPageModel model) throws IllegalArgumentException {
if (!model.chanName.equals(getChanName())) throw new IllegalArgumentException("wrong chan");
if (model.type == UrlPageModel.TYPE_CATALOGPAGE) return getUsingUrl() + model.boardName + "/catalog.html";
if (model.type == UrlPageModel.TYPE_BOARDPAGE && model.boardPage == 1) return (getUsingUrl() + model.boardName + "/");
return WakabaUtils.buildUrl(model, getUsingUrl());
}
@Override
public UrlPageModel parseUrl(String url) throws IllegalArgumentException {
String urlPath = UrlPathUtils.getUrlPath(url, getAllDomains());
if (urlPath == null) throw new IllegalArgumentException("wrong domain");
if (url.contains("/catalog.html")) {
try {
int index = url.indexOf("/catalog.html");
String left = url.substring(0, index);
UrlPageModel model = new UrlPageModel();
model.chanName = getChanName();
model.type = UrlPageModel.TYPE_CATALOGPAGE;
model.boardName = left.substring(left.lastIndexOf('/') + 1);
model.catalogType = 0;
return model;
} catch (Exception e) {}
}
UrlPageModel model = WakabaUtils.parseUrlPath(urlPath, getChanName());
if (model.type == UrlPageModel.TYPE_BOARDPAGE && model.boardPage == 0) model.boardPage = 1;
return model;
}
@Override
public void downloadFile(String url, OutputStream out, ProgressListener listener, CancellableTask task) throws Exception {
try {
super.downloadFile(url, out, listener, task);
} catch (HttpWrongStatusCodeException e) {
if (url.contains("/thumb/") && url.endsWith(".jpg") && e.getStatusCode() == 404) {
super.downloadFile(url.substring(0, url.length() - 3) + "png", out, listener, task);
} else {
throw e;
}
}
}
@Override
public String fixRelativeUrl(String url) {
if (url.startsWith("?/")) url = url.substring(1);
return super.fixRelativeUrl(url);
}
protected static class VichanAntiBot {
public static List<Pair<String, String>> getFormValues(String url, CancellableTask task, HttpClient httpClient) throws Exception {
return getFormValues(url, HttpRequestModel.DEFAULT_GET, task, httpClient, "<form name=\"post\"", "</form>");
}
public static List<Pair<String, String>> getFormValues(String url, HttpRequestModel requestModel, CancellableTask task, HttpClient client,
String startForm, String endForm) throws Exception {
VichanAntiBot reader = null;
HttpRequestModel request = requestModel;
HttpResponseModel response = null;
try {
response = HttpStreamer.getInstance().getFromUrl(url, request, client, null, task);
reader = new VichanAntiBot(response.stream, startForm, endForm);
return reader.readForm();
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {}
}
if (response != null) response.release();
}
}
private StringBuilder readBuffer = new StringBuilder();
private List<Pair<String, String>> result = null;
private String currentName = null;
private String currentValue = null;
private boolean currentTextarea = false;
private boolean currentReading = false;
private final char[] start;
private final char[][] filters;
private final Reader in;
private static final int FILTER_INPUT_OPEN = 0;
private static final int FILTER_TEXTAREA_OPEN = 1;
private static final int FILTER_NAME_OPEN = 2;
private static final int FILTER_VALUE_OPEN = 3;
private static final int FILTER_TAG_CLOSE = 4;
private static final int FILTER_TAG_BEFORE_CLOSE = 5;
private VichanAntiBot(InputStream in, String start, String end) {
this.start = start.toCharArray();
this.filters = new char[][] {
"<input".toCharArray(),
"<textarea".toCharArray(),
"name=\"".toCharArray(),
"value=\"".toCharArray(),
">".toCharArray(),
"/".toCharArray(),
end.toCharArray()
};
this.in = new BufferedReader(new InputStreamReader(in));
}
private List<Pair<String, String>> readForm() throws IOException {
result = new ArrayList<>();
skipUntilSequence(start);
int filtersCount = filters.length;
int[] pos = new int[filtersCount];
int[] len = new int[filtersCount];
for (int i=0; i<filtersCount; ++i) len[i] = filters[i].length;
int curChar;
while ((curChar = in.read()) != -1) {
for (int i=0; i<filtersCount; ++i) {
if (curChar == filters[i][pos[i]]) {
++pos[i];
if (pos[i] == len[i]) {
if (i == filtersCount - 1) {
return result;
}
handleFilter(i);
pos[i] = 0;
}
} else {
if (pos[i] != 0) pos[i] = curChar == filters[i][0] ? 1 : 0;
}
}
}
return result;
}
private void handleFilter(int i) throws IOException {
switch (i) {
case FILTER_INPUT_OPEN:
currentReading = true;
currentTextarea = false;
break;
case FILTER_TEXTAREA_OPEN:
currentReading = true;
currentTextarea = true;
break;
case FILTER_NAME_OPEN:
currentName = StringEscapeUtils.unescapeHtml4(readUntilSequence("\"".toCharArray()));
break;
case FILTER_VALUE_OPEN:
currentValue = StringEscapeUtils.unescapeHtml4(readUntilSequence("\"".toCharArray()));
break;
case FILTER_TAG_CLOSE:
if (currentTextarea) {
currentValue = StringEscapeUtils.unescapeHtml4(readUntilSequence("<".toCharArray()));
}
if (currentReading && currentName != null) result.add(Pair.of(currentName, currentValue != null ? currentValue : ""));
currentName = null;
currentValue = null;
currentReading = false;
currentTextarea = false;
break;
case FILTER_TAG_BEFORE_CLOSE: // <textarea ..... />
currentTextarea = false;
break;
}
}
private void skipUntilSequence(char[] sequence) throws IOException {
int len = sequence.length;
if (len == 0) return;
int pos = 0;
int curChar;
while ((curChar = in.read()) != -1) {
if (curChar == sequence[pos]) {
++pos;
if (pos == len) break;
} else {
if (pos != 0) pos = curChar == sequence[0] ? 1 : 0;
}
}
}
private String readUntilSequence(char[] sequence) throws IOException {
int len = sequence.length;
if (len == 0) return "";
readBuffer.setLength(0);
int pos = 0;
int curChar;
while ((curChar = in.read()) != -1) {
readBuffer.append((char) curChar);
if (curChar == sequence[pos]) {
++pos;
if (pos == len) break;
} else {
if (pos != 0) pos = curChar == sequence[0] ? 1 : 0;
}
}
int buflen = readBuffer.length();
if (buflen >= len) {
readBuffer.setLength(buflen - len);
return readBuffer.toString();
} else {
return "";
}
}
public void close() throws IOException {
in.close();
}
}
}