Java tutorial
package eu.nerdz.api.impl.reverse.messages; /* This file is part of NerdzApi-java. NerdzApi-java 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. NerdzApi-java 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 NerdzApi-java. If not, see <http://www.gnu.org/licenses/>. (C) 2013 Marco Cilloni <marco.cilloni@yahoo.com> */ import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import eu.nerdz.api.BadStatusException; import eu.nerdz.api.ContentException; import eu.nerdz.api.HttpException; import eu.nerdz.api.UserInfo; import eu.nerdz.api.impl.reverse.AbstractReverseApplication; import eu.nerdz.api.messages.Conversation; import eu.nerdz.api.messages.ConversationHandler; import eu.nerdz.api.messages.Message; import eu.nerdz.api.messages.MessageFetcher; public class ReverseConversationHandler implements ConversationHandler { private static final long serialVersionUID = -798135247830642378L; protected final ReverseMessenger mMessenger; public ReverseConversationHandler(ReverseMessenger messenger) { this.mMessenger = messenger; } @Override @SuppressWarnings("unchecked") public List<Conversation> getConversations() throws IOException, HttpException, ContentException { return (List) this.getConversationsAsFetchers(); //Yes, I know, it's ugly. But do I really have to create a new list, when the one I have is already what I need? } @Override public List<MessageFetcher> getConversationsAsFetchers() throws IOException, HttpException, ContentException { List<MessageFetcher> conversations = null; List<String> rows = this.parseTableRows(this.mMessenger.get("/pages/pm/inbox.html.php")); if (rows != null) { conversations = new ArrayList<MessageFetcher>(20); for (String row : rows) conversations.add(this.parseConversationRow(row)); } return conversations; } @Override public List<Message> getMessages(Conversation conversation) throws IOException, HttpException, ContentException { return this.getMessages(conversation, 0, 10); } @Override public List<Message> getMessages(Conversation conversation, int start, int howMany) throws IOException, HttpException, ContentException { return this.getMessagesAndCheck(conversation, start, howMany).getLeft(); } @Override public Message getLastMessage(Conversation conversation) throws IOException, HttpException, ContentException { List<Message> messages = this.getMessages(conversation, 0, 1); if (messages.size() != 1) { throw new ContentException( "Something is very broken in NERDZ. Report to nessuno ASAP that read.html.php?action=conversation is broken"); } return messages.get(0); } /** * Fetches messages and returns a pair containing a list of messages of given conversation and also a Boolean representing the fact that the conversation has still messages to be read. * Not working properly in Reverse (but working great in FastReverse) * @param conversation * @param start * @param howMany * @return * @throws IOException * @throws HttpException * @throws ContentException */ protected Pair<List<Message>, Boolean> getMessagesAndCheck(Conversation conversation, int start, int howMany) throws IOException, HttpException, ContentException { if (howMany > 10) { howMany = 10; } List<Message> messages; HashMap<String, String> form = new HashMap<String, String>(4); form.put("from", String.valueOf(conversation.getOtherID())); form.put("to", String.valueOf(this.mMessenger.getUserID())); form.put("start", String.valueOf(start / howMany)); form.put("num", String.valueOf(howMany)); UserInfo userInfo = this.mMessenger.getUserInfo(); String body = this.mMessenger.post("/pages/pm/read.html.php?action=conversation", form); int endOfList = body.indexOf("<form id=\"convfrm\""); Boolean hasMore = body.contains("class=\"more_btn\" href=\"#\">"); switch (endOfList) { case 0: return null; case -1: { if (start == 0) throw new ContentException("malformed response: " + body); } default: { int headOfList = body.indexOf("<div style=\"margin-top: 3px\" id=\"pm"); if (headOfList < 0) { throw new ContentException("malformed response: " + body); } messages = new LinkedList<Message>(); for (String messageString : this.splitMessages( endOfList > 0 ? body.substring(headOfList, endOfList) : body.substring(headOfList))) messages.add(new ReverseMessage(conversation, userInfo, this.parseDate(messageString), this.parseMessage(messageString), this.parseSender(messageString).equals(conversation.getOtherName()))); } } return new ImmutablePair<List<Message>, Boolean>(messages, hasMore); } @Override public void deleteConversation(Conversation conversation) throws IOException, HttpException, BadStatusException, ContentException { HashMap<String, String> form = new HashMap<String, String>(2); form.put("from", String.valueOf(conversation.getOtherID())); form.put("to", String.valueOf(this.mMessenger.getUserID())); try { JSONObject response = new JSONObject(this.mMessenger.post("/pages/pm/delete.json.php", form, AbstractReverseApplication.NERDZ_DOMAIN_NAME + "/pm.php")); if (!(response.getString("status").equals("ok") && response.getString("message").equals("OK"))) { throw new BadStatusException("wrong status couple (" + response.getString("status") + ", " + response.getString("message") + "), conversation not deleted"); } } catch (JSONException e) { throw new ContentException("Error while parsing JSON in new messages: " + e.getLocalizedMessage()); } } @Override public MessageFetcher createFetcher(Conversation conversation) { return new ReverseMessageFetcher(conversation); } /** * This method parses the timestamp from a raw message, and returns it as a Date. * * @param messageString the raw message string parsed by splitMessage * @return A Date representing the moment this message has been sent. * @throws ContentException */ private Date parseDate(String messageString) throws ContentException { int timestampPosition = messageString.indexOf("data-timestamp=\""); if (timestampPosition < 0) { throw new ContentException("malformed response: " + messageString); } timestampPosition += 16; return new Date(Long.parseLong( messageString.substring(timestampPosition, messageString.indexOf('"', timestampPosition))) * 1000L); } /** * This method parses the sender's ID from a raw message, and returns it as an int. * * @param messageString the raw message string parsed by splitMessage * @return An int containing the sender's ID. * @throws ContentException */ private int parseSenderID(String messageString) throws ContentException { int fromIDPosition = messageString.indexOf("data-fromid=\""); if (fromIDPosition < 0) { throw new ContentException("malformed response: " + messageString); } fromIDPosition += 13; return Integer .parseInt(messageString.substring(fromIDPosition, messageString.indexOf('"', fromIDPosition))); } /** * This method parses the sender's username from a raw message. * * @param messageString the raw message string parsed by splitMessage * @return A String containing the sender's username * @throws ContentException */ private String parseSender(String messageString) throws ContentException { int closeLinkPosition = messageString.lastIndexOf("</a>", messageString.lastIndexOf("<time")); if (closeLinkPosition < 0) { throw new ContentException("malformed response: " + messageString); } int nickStart = messageString.lastIndexOf('>', closeLinkPosition) + 1; if (nickStart < 0) { throw new ContentException("malformed response: " + messageString); } return StringEscapeUtils.unescapeHtml4(messageString.substring(nickStart, closeLinkPosition)); } /** * This method parses the message from a raw message string. * * @param messageString the raw message string parsed by splitMessage * @return A message * @throws ContentException */ private String parseMessage(String messageString) throws ContentException { int msgStart = messageString.lastIndexOf("1pt solid #FFF\">") + 16; if (msgStart <= 0) { throw new ContentException("malformed message string: " + messageString); } return StringEscapeUtils.unescapeHtml4(this.removeTags(messageString.substring(msgStart))); } /** * Parses and removes all HTML tags in a message. * * @param msg A message parsed by parseMessage * @return The parsed message */ private String removeTags(String msg) { return this.quoteParse(this.imgParse(this.linkParse(this.ytParse( this.removeDivs(msg.replaceAll("<br />", "\n").replaceAll("<hr style=\"clear:both\" />", "\n")))))); } /** * Finds links and replaces them with its URL. * * @param msg A message parsed by parseMessage * @return The parsed message */ private String linkParse(String msg) { int linkPos, hrefPos, hrefEnd, linkEnd; while ((linkPos = msg.indexOf("<a ")) >= 0) { hrefPos = msg.indexOf("href=\"", linkPos) + 6; hrefEnd = msg.indexOf('"', hrefPos); linkEnd = msg.indexOf("</a>", hrefEnd) + 4; msg = msg.substring(0, linkPos) + msg.substring(hrefPos, hrefEnd) + msg.substring(linkEnd); } return msg; } /** * Removes divs. * * @param msg A message parsed by parseMessage * @return The parsed message */ private String removeDivs(String msg) { int divPos, divEnd; while ((divPos = msg.indexOf("<div")) >= 0) { divEnd = msg.indexOf('>', divPos) + 1; msg = msg.substring(0, divPos) + msg.substring(divEnd); } return msg; } /** * Finds YouTube videos and replaces them with the video's url. * * @param msg A message parsed by parseMessage * @return The parsed message */ private String ytParse(String msg) { int iframePos, srcPos, srcEnd, iframeEnd; while ((iframePos = msg.indexOf("<iframe")) >= 0) { srcPos = msg.indexOf("src=\"", iframePos) + 5; srcEnd = msg.indexOf('"', srcPos); iframeEnd = msg.indexOf("</iframe>", srcEnd) + 9; msg = msg.substring(0, iframePos) + this.fixYTLink(msg.substring(srcPos, srcEnd)) + msg.substring(iframeEnd); } return msg; } /** * Fixes the embedded YouTube link to a regular youtu.be one. * * @param link the raw link * @return The parsed message */ private String fixYTLink(String link) { return "http://youtu.be/" + link.substring(link.lastIndexOf('/') + 1, link.lastIndexOf('?')); } /** * Parses images, replacing them with just URLs. * * @param msg A message parsed by parseMessage * @return The parsed message */ private String imgParse(String msg) { int imgPos, srcPos, srcEnd, imgEnd; while ((imgPos = msg.indexOf("<img")) >= 0) { srcPos = msg.indexOf("src=\"", imgPos) + 5; srcEnd = msg.indexOf('"', srcPos); imgEnd = msg.indexOf("/>", srcEnd) + 2; msg = msg.substring(0, imgPos) + msg.substring(srcPos, srcEnd) + msg.substring(imgEnd); } return msg; } private String quoteParse(String msg) { int spanPos, quotePos, quoteEnd, spanEnd; while ((spanPos = msg.indexOf("<span style=\"float: left;")) >= 0) { quotePos = msg.indexOf("left: 3%\">", spanPos) + 10; quoteEnd = msg.indexOf("</blockquote>", quotePos); spanEnd = msg.indexOf("</span></div>", quoteEnd) + 13; msg = msg.substring(0, spanPos) + "\n<i>\"" + msg.substring(quotePos, quoteEnd) + "\"</i>\n" + msg.substring(spanEnd); } return msg; } /** * Splits messages from a response into raw strings. * * @param list a raw list of messages * @return a list containing raw message strings. * @throws ContentException */ private List<String> splitMessages(String list) throws ContentException { int lastCloseDivs, lastMessagePosition = 0; List<String> messages = new LinkedList<String>(); while ((lastCloseDivs = list.indexOf("</div></div>", lastMessagePosition)) > 0) { messages.add(list.substring(lastMessagePosition, lastCloseDivs)); lastMessagePosition = lastCloseDivs + 12; } return messages; } /** * Parses Conversations table, returning raw conversation strings as a list. * * @param table a string containing a raw conversation HTML table. * @return a list of raw conversation strings. */ private List<String> parseTableRows(String table) { List<String> conversations = new ArrayList<String>(20); //If no conversation is present, return null int beginning = table.indexOf("<tr "); if (beginning < 0) { return null; } //Everything except conversations table should be removed table = table.substring(beginning, table.indexOf("</table>")); int tdIndex, endTrIndex = 0; while ((tdIndex = table.indexOf("<td", endTrIndex)) != -1) { endTrIndex = table.indexOf("</tr>", endTrIndex + 5); //add 5 to last value found conversations.add(table.substring(tdIndex, endTrIndex)); } return conversations; } /** * Parses a raw conversation string, returning a Conversation. * * @param row a raw conversation string * @return A Conversation containing data parsed from row. * @throws ContentException */ private MessageFetcher parseConversationRow(String row) throws ContentException { int otherNamePosition = row.indexOf("<a href="); if (otherNamePosition < 0) { throw new ContentException("Malformed content \"" + row + "\""); } String otherName = StringEscapeUtils .unescapeHtml4(row.substring(row.indexOf('>', otherNamePosition) + 1, row.indexOf("</a>"))); int dataFromPosition = row.indexOf("data-from=\""); if (dataFromPosition < 0) { throw new ContentException("Malformed content \"" + row + "\""); } dataFromPosition += 11; int dataFrom = Integer.parseInt(row.substring(dataFromPosition, row.indexOf('"', dataFromPosition))); int dataTimePosition = row.indexOf("data-timestamp=\""); if (dataTimePosition < 0) { throw new ContentException("Malformed content \"" + row + "\""); } dataTimePosition += 16; Date lastDate = new Date( Long.parseLong(row.substring(dataTimePosition, row.indexOf('"', dataTimePosition))) * 1000L); return new ReverseMessageFetcher(otherName, dataFrom, lastDate); } private class ReverseMessageFetcher extends ReverseConversation implements MessageFetcher { List<Message> mMessageList; private int mFetchStart; private int mIterateStart; private boolean mEndReached; public ReverseMessageFetcher(Conversation conversation) { this(conversation.getOtherName(), conversation.getOtherID(), conversation.getLastDate()); this.reset(); } public ReverseMessageFetcher(String userName, int userID, Date lastDate) { super(userName, userID, lastDate); } @Override public int fetch() throws IOException, HttpException, ContentException { return this.fetch(10); } @Override public int fetch(int limit) throws IOException, HttpException, ContentException { Pair<List<Message>, Boolean> fetchedPair = ReverseConversationHandler.this.getMessagesAndCheck(this, this.mFetchStart, 10); List<Message> fetched = fetchedPair.getLeft(); if (this.mMessageList != null) { fetched.addAll(this.mMessageList); } this.mMessageList = fetched; this.mFetchStart = fetched.size(); this.mEndReached = !fetchedPair.getRight(); return this.mFetchStart; } @Override public int getIterateStart() { return this.mIterateStart; } @Override public int getFetchStart() { return this.mFetchStart; } @Override public List<Message> getFetchedMessages() { this.mIterateStart = this.mFetchStart; return this.mMessageList; } @Override public void setStart(int start) { this.mFetchStart = this.mIterateStart = start; } @Override public void reset() { this.mFetchStart = 0; this.mIterateStart = 0; this.mEndReached = false; this.mMessageList = null; } @Override public Boolean hasMore() { return !this.mEndReached; } @Override public Iterator<Message> iterator() { return new ReverseMessageFetcherIterator(); } private class ReverseMessageFetcherIterator implements Iterator<Message> { @Override public boolean hasNext() { return ReverseMessageFetcher.this.mIterateStart < ReverseMessageFetcher.this.mMessageList.size(); } @Override public Message next() { return ReverseMessageFetcher.this.mMessageList.get(ReverseMessageFetcher.this.mIterateStart++); } @Override public void remove() { throw new UnsupportedOperationException("remove() is not supported"); } } } }