Java tutorial
/** * WikiUtils.java * Copyright (C)2009 Thomas Hirsch * Geohashdroid Copyright (C)2009 Nicholas Killewald * * This file is distributed under the terms of the BSD license. * The source package should have a LICENSE file at the toplevel. */ package net.exclaimindustries.geohashdroid.wiki; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import javax.xml.parsers.DocumentBuilderFactory; import net.exclaimindustries.geohashdroid.R; import net.exclaimindustries.geohashdroid.UnitConverter; import net.exclaimindustries.geohashdroid.util.Graticule; import net.exclaimindustries.geohashdroid.util.Info; import net.exclaimindustries.tools.DOMUtil; import net.exclaimindustries.tools.DateTools; import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource; import org.apache.commons.httpclient.methods.multipart.FilePart; import org.apache.commons.httpclient.methods.multipart.MultipartEntity; import org.apache.commons.httpclient.methods.multipart.Part; import org.apache.commons.httpclient.methods.multipart.StringPart; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicNameValuePair; import org.w3c.dom.Document; import org.w3c.dom.Element; import android.content.Context; import android.text.format.DateFormat; import android.util.Log; /** Various stateless utility methods to query a mediawiki server */ public class WikiUtils { /** The base URL for all wiki activities. Remember the trailing slash! */ private static final String WIKI_BASE_URL = "http://wiki.xkcd.com/wgh/"; /** The URL for the MediaWiki API. There's no trailing slash here. */ private static final String WIKI_API_URL = WIKI_BASE_URL + "api.php"; private static final String DEBUG_TAG = "WikiUtils"; // The most recent request issued by WikiUtils. This allows the abort() // method to work. private static HttpUriRequest mLastRequest; /** * Aborts the current wiki request. Well, technically, it's the most recent * wiki request. If it's already done, nothing happens. This will, of * course, cause exceptions in whatever's servicing the request. */ public static void abort() { if (mLastRequest != null) mLastRequest.abort(); } /** * Returns the wiki base URL. That is, the base of where all requests will * be sent. * * @return the wiki base URL */ public static String getWikiBaseUrl() { return WIKI_BASE_URL; } /** * Returns the URL for the MediaWiki API. This is where any queries should * go, in standard HTTP query form. * * @return the MediaWiki API URL */ public static String getWikiApiUrl() { return WIKI_API_URL; } /** * Returns the content of a http request as an XML Document. This is to be * used only when we know the response to a request will be XML. Otherwise, * this will probably throw an exception. * * @param httpclient an active HTTP session * @param httpreq an HTTP request (GET or POST) * @return a Document containing the contents of the response */ private static Document getHttpDocument(HttpClient httpclient, HttpUriRequest httpreq) throws Exception { // Remember the last request. We might want to abort it later. mLastRequest = httpreq; HttpResponse response = httpclient.execute(httpreq); HttpEntity entity = response.getEntity(); return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(entity.getContent()); } /** * Returns whether or not a given wiki page or file exists. * * @param httpclient an active HTTP session * @param pagename the name of the wiki page * @return true if the page exists, false if not * @throws WikiException problem with the wiki, translate the ID * @throws Exception anything else happened, use getMessage */ public static boolean doesWikiPageExist(HttpClient httpclient, String pagename) throws Exception { // It's GET time! This is basically the same as the content request, but // we really don't need ANY data other than whether or not the page // exists, so we won't call for anything. HttpGet httpget = new HttpGet( WIKI_API_URL + "?action=query&format=xml&titles=" + URLEncoder.encode(pagename, "UTF-8")); Document response = getHttpDocument(httpclient, httpget); // Now for some of the usual checking that should look familiar... Element root = response.getDocumentElement(); // Error check! if (doesResponseHaveError(root)) { throw new WikiException(getErrorTextId(findErrorCode(root))); } Element pageElem; try { pageElem = DOMUtil.getFirstElement(root, "page"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } // "invalid" or "missing" both resolve to the same answer: No. Anything // else means yes. if (pageElem.hasAttribute("invalid") || pageElem.hasAttribute("missing")) return false; else return true; } /** * Returns the raw content of a wiki page in a single string. Optionally, * also attaches the fields for future resubmission to a HashMap (namely, an * edittoken and a timestamp). * * @param httpclient an active HTTP session * @param pagename the name of the wiki page * @param formfields if not null, this hashmap will be filled with the correct HTML form fields to resubmit the page. * @return the raw code of the wiki page, or null if the page doesn't exist * @throws WikiException problem with the wiki, translate the ID * @throws Exception anything else happened, use getMessage */ public static String getWikiPage(HttpClient httpclient, String pagename, HashMap<String, String> formfields) throws Exception { // We can use a GET statement here. HttpGet httpget = new HttpGet( WIKI_API_URL + "?action=query&format=xml&prop=" + URLEncoder.encode("info|revisions", "UTF-8") + "&rvprop=content&format=xml&intoken=edit&titles=" + URLEncoder.encode(pagename, "UTF-8")); String page; Document response = getHttpDocument(httpclient, httpget); // Good, good. First, figure out if the page even exists. Element root = response.getDocumentElement(); // Error check! if (doesResponseHaveError(root)) { throw new WikiException(getErrorTextId(findErrorCode(root))); } Element pageElem; Element text; try { pageElem = DOMUtil.getFirstElement(root, "page"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } // If we got an "invalid" attribute, the page not only doesn't exist, but it // CAN'T exist, and is therefore an error. if (pageElem.hasAttribute("invalid")) throw new WikiException(R.string.wiki_error_invalid_page); if (formfields != null) { // If we have a formfields hash ready, populate it with a couple values. formfields.put("summary", "An expedition message sent via Geohash Droid for Android."); if (pageElem.hasAttribute("edittoken")) formfields.put("token", DOMUtil.getSimpleAttributeText(pageElem, "edittoken")); if (pageElem.hasAttribute("touched")) formfields.put("basetimestamp", DOMUtil.getSimpleAttributeText(pageElem, "touched")); } // If we got a "missing" attribute, the page hasn't been made yet, so we // return null. if (pageElem.hasAttribute("missing")) return null; // Otherwise, get the text and fill out the form fields. try { text = DOMUtil.getFirstElement(pageElem, "rev"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } page = DOMUtil.getSimpleElementText(text); return page; } /** Replaces an entire wiki page @param httpclient an active HTTP session @param pagename the name of the wiki page @param content the new content of the wiki page to be submitted @param formfields a hashmap with the fields needed (besides pagename and content; those will be filled in this method) @throws WikiException problem with the wiki, translate the ID @throws Exception anything else happened, use getMessage */ public static void putWikiPage(HttpClient httpclient, String pagename, String content, HashMap<String, String> formfields) throws Exception { // If there's no edit token in the hash map, we can't do anything. if (!formfields.containsKey("token")) { throw new WikiException(R.string.wiki_error_protected); } HttpPost httppost = new HttpPost(WIKI_API_URL); ArrayList<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("action", "edit")); nvps.add(new BasicNameValuePair("title", pagename)); nvps.add(new BasicNameValuePair("text", content)); nvps.add(new BasicNameValuePair("format", "xml")); for (String s : formfields.keySet()) { nvps.add(new BasicNameValuePair(s, formfields.get(s))); } httppost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8")); Document response = getHttpDocument(httpclient, httppost); Element root = response.getDocumentElement(); // First, check for errors. if (doesResponseHaveError(root)) { throw new WikiException(getErrorTextId(findErrorCode(root))); } // And really, that's it. We're done! } /** Uploads an image to the wiki @param httpclient an active HTTP session, wiki login has to have happened before. @param filename the name of the new image file @param description the description of the image. An initial description will be used as page content for the image's wiki page @param formfields a formfields hash as modified by getWikiPage containing an edittoken we can use (see the MediaWiki API for reasons why) @param data a ByteArray containing the raw image data (assuming jpeg encoding, currently). */ public static void putWikiImage(HttpClient httpclient, String filename, String description, HashMap<String, String> formfields, byte[] data) throws Exception { if (!formfields.containsKey("token")) { throw new WikiException(R.string.wiki_error_unknown); } HttpPost httppost = new HttpPost(WIKI_API_URL); // First, we need an edit token. Let's get one. ArrayList<NameValuePair> tnvps = new ArrayList<NameValuePair>(); tnvps.add(new BasicNameValuePair("action", "query")); tnvps.add(new BasicNameValuePair("prop", "info")); tnvps.add(new BasicNameValuePair("intoken", "edit")); tnvps.add(new BasicNameValuePair("titles", "UPLOAD_AN_IMAGE")); tnvps.add(new BasicNameValuePair("format", "xml")); httppost.setEntity(new UrlEncodedFormEntity(tnvps, "utf-8")); Document response = getHttpDocument(httpclient, httppost); Element root = response.getDocumentElement(); // Hopefully, a token exists. If not, a problem exists. String token; Element page; try { page = DOMUtil.getFirstElement(root, "page"); token = DOMUtil.getSimpleAttributeText(page, "edittoken"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } // TOKEN GET! Now we've got us enough to get our upload on! Part[] nvps = new Part[] { new StringPart("action", "upload", "utf-8"), new StringPart("filename", filename, "utf-8"), new StringPart("comment", description, "utf-8"), new StringPart("watch", "true", "utf-8"), new StringPart("ignorewarnings", "true", "utf-8"), new StringPart("token", token, "utf-8"), new StringPart("format", "xml", "utf-8"), new FilePart("file", new ByteArrayPartSource(filename, data), "image/jpeg", "utf-8"), }; httppost.setEntity(new MultipartEntity(nvps, httppost.getParams())); response = getHttpDocument(httpclient, httppost); root = response.getDocumentElement(); // First, check for errors. if (doesResponseHaveError(root)) { throw new WikiException(getErrorTextId(findErrorCode(root))); } } /** * Retrieves valid login cookies for an HTTP session. These will be added to * the HttpClient value passed in, so re-use it for future wiki transactions. * * @param httpclient an active HTTP session. * @param wpName a wiki user name. * @param wpPassword the matching password to this user name. * @throws WikiException problem with the wiki, translate the ID * @throws Exception anything else happened, use getMessage */ public static void login(HttpClient httpclient, String wpName, String wpPassword) throws Exception { HttpPost httppost = new HttpPost(WIKI_API_URL); ArrayList<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("action", "login")); nvps.add(new BasicNameValuePair("lgname", wpName)); nvps.add(new BasicNameValuePair("lgpassword", wpPassword)); nvps.add(new BasicNameValuePair("format", "xml")); httppost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8")); Log.d(DEBUG_TAG, "Trying login..."); Document response = getHttpDocument(httpclient, httppost); // The result comes in as an XML chunk. Since we're expecting the cookies // to be set properly, all we care about is the "result" attribute of the // "login" element. Element root = response.getDocumentElement(); Element login; String result; try { login = DOMUtil.getFirstElement(root, "login"); result = DOMUtil.getSimpleAttributeText(login, "result"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } // Now, get the result. If it was a success, cookies got added. If it was // NeedToken, this is a 1.16 wiki (as it should be now) and we need another // request to get the final token. if (result.equals("NeedToken")) { Log.d(DEBUG_TAG, "Token needed, trying again..."); // Okay, do the same thing again, this time with the token we got the // first time around. Cookies will be set this time around, I think. String token = DOMUtil.getSimpleAttributeText(login, "token"); httppost = new HttpPost(WIKI_API_URL); nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("action", "login")); nvps.add(new BasicNameValuePair("lgname", wpName)); nvps.add(new BasicNameValuePair("lgpassword", wpPassword)); nvps.add(new BasicNameValuePair("lgtoken", token)); nvps.add(new BasicNameValuePair("format", "xml")); httppost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8")); Log.d(DEBUG_TAG, "Sending it out..."); response = getHttpDocument(httpclient, httppost); Log.d(DEBUG_TAG, "Response has returned!"); // Again! root = response.getDocumentElement(); try { login = DOMUtil.getFirstElement(root, "login"); result = DOMUtil.getSimpleAttributeText(login, "result"); } catch (Exception e) { throw new WikiException(R.string.wiki_error_xml); } } // Check it. If NeedToken was returned again, then the wiki is just telling // us nonsense and we've got a right to throw an exception. if (result.equals("Success")) { Log.d(DEBUG_TAG, "Success!"); return; } else { Log.d(DEBUG_TAG, "FAILURE!"); throw new WikiException(getErrorTextId(result)); } } /** * Gets the text ID that corresponds to a given error code. If the code isn't * recognized, this returns wiki_error_unknown instead. Note that this WON'T * understand a non-error condition; check to make sure it isn't first. * * @param code String returned from the wiki * @return text ID that corresponds to that error */ private static int getErrorTextId(String code) { // If we don't recognize the error (or shouldn't get it at all), we use // this, because we don't have the slightest clue what's wrong. int error = R.string.wiki_error_unknown; // First, general errors. These are the only general ones we care about; // there's more, but those aren't likely to come up. if (code.equals("unsupportednamespace")) error = R.string.wiki_error_illegal_namespace; else if (code.equals("protectednamespace-interface") || code.equals("protectednamespace") || code.equals("customcssjsprotected") || code.equals("cascadeprotected") || code.equals("protectedpage")) error = R.string.wiki_error_protected; else if (code.equals("confirmemail")) error = R.string.wiki_error_email_confirm; else if (code.equals("permissiondenied")) error = R.string.wiki_error_permission_denied; else if (code.equals("blocked") || code.equals("autoblocked")) error = R.string.wiki_error_blocked; else if (code.equals("ratelimited")) error = R.string.wiki_error_rate_limit; else if (code.equals("readonly")) error = R.string.wiki_error_read_only; // Then, login errors. These come from the result attribute. else if (code.equals("Illegal") || code.equals("NoName") || code.equals("CreateBlocked")) error = R.string.wiki_error_bad_username; else if (code.equals("EmptyPass") || code.equals("WrongPass") || code.equals("WrongPluginPass")) error = R.string.wiki_error_bad_password; else if (code.equals("Throttled")) error = R.string.wiki_error_throttled; // Next, edit errors. These come from the error element, code attribute. else if (code.equals("protectedtitle")) error = R.string.wiki_error_protected; else if (code.equals("cantcreate") || code.equals("cantcreate-anon")) error = R.string.wiki_error_no_create; else if (code.equals("spamdetected")) error = R.string.wiki_error_spam; else if (code.equals("filtered")) error = R.string.wiki_error_filtered; else if (code.equals("contenttoobig")) error = R.string.wiki_error_too_big; else if (code.equals("noedit") || code.equals("noedit-anon")) error = R.string.wiki_error_no_edit; else if (code.equals("editconflict")) error = R.string.wiki_error_conflict; // If all else fails, log what we got. else Log.d(DEBUG_TAG, "Unknown error code came back: " + code); return error; } private static boolean doesResponseHaveError(Element elem) { try { DOMUtil.getFirstElement(elem, "error"); } catch (Exception ex) { return false; } return true; } private static String findErrorCode(Element elem) { try { Element error = DOMUtil.getFirstElement(elem, "error"); return DOMUtil.getSimpleAttributeText(error, "code"); } catch (Exception ex) { return "UnknownError"; } } /** * Retrieves the wiki page name for the given data. This accounts for * globalhashes, too. * * @param info Info from which a page name will be derived * @return said pagename */ public static String getWikiPageName(Info info) { String date = DateTools.getHyphenatedDateString(info.getCalendar()); if (info.isGlobalHash()) { return date + "_global"; } else { Graticule grat = info.getGraticule(); String lat = grat.getLatitudeString(true); String lon = grat.getLongitudeString(true); return date + "_" + lat + "_" + lon; } } /** * Retrieves the text for the Expedition template appropriate for the given * Info. * * TODO: The wiki doesn't appear to have an Expedition template for * globalhashing yet. * * @param info Info from which an Expedition template will be generated * @param c Context so we can grab the globalhash template if we need it * @return said template */ public static String getWikiExpeditionTemplate(Info info, Context c) { String date = DateTools.getHyphenatedDateString(info.getCalendar()); if (info.isGlobalHash()) { // Until a proper template can be made in the wiki itself, we'll have // to settle for this... InputStream is = c.getResources().openRawResource(R.raw.globalhash_template); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); // Now, read in each line and do all substitutions on it. String input; StringBuffer toReturn = new StringBuffer(); try { while ((input = br.readLine()) != null) { input = input.replaceAll("%%LATITUDE%%", UnitConverter.makeLatitudeCoordinateString(c, info.getLatitude(), true, UnitConverter.OUTPUT_DETAILED)); input = input.replaceAll("%%LONGITUDE%%", UnitConverter.makeLongitudeCoordinateString(c, info.getLongitude(), true, UnitConverter.OUTPUT_DETAILED)); input = input.replaceAll("%%LATITUDEURL%%", Double.valueOf(info.getLatitude()).toString()); input = input.replaceAll("%%LONGITUDEURL%%", Double.valueOf(info.getLongitude()).toString()); input = input.replaceAll("%%DATENUMERIC%%", date); input = input.replaceAll("%%DATESHORT%%", DateFormat.format("E MMM d yyyy", info.getCalendar()).toString()); input = input.replaceAll("%%DATEGOOGLE%%", DateFormat.format("d+MMM+yyyy", info.getCalendar()).toString()); toReturn.append(input).append("\n"); } } catch (IOException e) { // Don't do anything; just assume we're done. } return toReturn.toString() + getWikiCategories(info); } else { Graticule grat = info.getGraticule(); String lat = grat.getLatitudeString(true); String lon = grat.getLongitudeString(true); return "{{subst:Expedition|lat=" + lat + "|lon=" + lon + "|date=" + date + "}}"; } } /** * Retrieves the text for the categories to put on the wiki for pictures. * * @param info Info from which categories will be generated * @return said categories */ public static String getWikiCategories(Info info) { String date = DateTools.getHyphenatedDateString(info.getCalendar()); String toReturn = "[[Category:Meetup on " + date + "]]\n"; if (info.isGlobalHash()) { return toReturn + "[[Category:Globalhash]]"; } else { Graticule grat = info.getGraticule(); String lat = grat.getLatitudeString(true); String lon = grat.getLongitudeString(true); return toReturn + "[[Category:Meetup in " + lat + " " + lon + "]]"; } } }