User:A proofreader/MediaWiki.java

,	 *  and   can be embedded directly in	 * other class files that refer to the value, therefore the literal is * wrapped in a  to avoid this. */	public static final String VERSION = new String("0.01");

// - - - VARIABLES - - -

private final String host;

private final String scriptPath;

/**	 * Parses information returned by the MediaWiki API in XML format. */	private final transient DocumentBuilder documentBuilder;

/**	 * Contains cookies set by the wiki. An implementation of Map * placed here must also be Serializable. */	private final Map cookies = new TreeMap;

/**	 * Whether this MediaWiki attempts to retrieve compressed content * from the wiki it represents. */	private boolean useGzip;

/**	 * Lock used to ensure that only one thread can write to the preference * variables of this MediaWiki. To prevent deadlock in this class, * if an operation needs to lock both  and *,   must be acquired * last. */	private final transient ReadWriteLock preferenceLock = new ReentrantReadWriteLock;

/**	 * Lock used to ensure that only one thread can access the network to to * connect to the wiki represented by this MediaWiki. To prevent * deadlock in this class, if an operation needs to lock both *  and  , *  must be acquired first. */	private final transient Lock networkLock = new ReentrantLock;

// - - - CONSTRUCTORS, INITIALIZATION AND SERIALIZATION CODE - - -

{		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance; documentBuilderFactory.setCoalescing(true); documentBuilderFactory.setIgnoringComments(true); try { documentBuilder = documentBuilderFactory.newDocumentBuilder; } catch (ParserConfigurationException e) { throw new ExceptionInInitializerError(e); }	}

/**	 * Creates an instance of MediaWiki that performs actions on the * MediaWiki wiki at the given, whose script path is the * root of the wiki (http:// host /api.php). * 	 * @param host *           The hostname, IPv4 dotted decimal or IPv6 address of the wiki. * @throws NullPointerException *            if the   is  	 * @throws IllegalArgumentException *            if the   is an empty string */	public MediaWiki(final String host) throws NullPointerException, IllegalArgumentException { this(host, ""); }

/**	 * Creates an instance of MediaWiki</tt> that performs actions on the * MediaWiki wiki at the given, whose script path is the * given  (	 * http:// host  scriptPath /api.php</tt>). * 	 * @param host *           The hostname, IPv4 dotted decimal or IPv6 address of the wiki. * @param scriptPath *           The script path on the wiki which contains api.php</tt>. *           This may be empty, in which case the root of the wiki is used, *           or contain any number of leading and/or trailing slashes, *           which are removed. * @throws NullPointerException *            if the   or   is	 * * @throws IllegalArgumentException *            if the   is an empty string */	public MediaWiki(final String host, final String scriptPath) throws NullPointerException, IllegalArgumentException { if (host.length == 0) throw new IllegalArgumentException("host is empty"); if (scriptPath == null) throw new NullPointerException; this.host = host;

int beginIndex = 0, endIndex = scriptPath.length; while (beginIndex < endIndex && scriptPath.charAt(beginIndex) == '/') beginIndex++; while (endIndex > beginIndex && scriptPath.charAt(endIndex - 1) == '/') endIndex--; this.scriptPath = new String(scriptPath.substring(beginIndex, endIndex)); }

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.registerValidation(this, 0); in.defaultReadObject; }

/**	 * This method validates deserialized objects and should generally not be * called by applications. */	@Override public void validateObject throws InvalidObjectException { if (host == null) throw new InvalidObjectException("host == null"); if (scriptPath == null) throw new InvalidObjectException("scriptPath == null"); if (host.length == 0) throw new InvalidObjectException("host is empty"); if (scriptPath.charAt(0) == '/' || scriptPath.charAt(scriptPath.length - 1) == '/') throw new InvalidObjectException("scriptPath starts or ends with /"); }

// - - - CONNECTION PREFERENCES - - -

/**	 * Returns whether this MediaWiki</tt> attempts to retrieve compressed * content from the wiki it represents. * 	 * @return whether this MediaWiki</tt> attempts to retrieve compressed *        content from the wiki it represents */	public boolean isUsingCompression { preferenceLock.readLock.lock; try { return useGzip; } finally { preferenceLock.readLock.unlock; }	}

/**	 * Sets whether this MediaWiki</tt> is to attempt to retrieve compressed * content from the wiki it represents. * 	 * @param newValue *             if this MediaWiki</tt> is to attempt to	 *            retrieve compressed content from the wiki it represents; *             if compression should be avoided. The value *           of   is useful if using MediaWiki</tt> to	 *            edit a wiki on a local network, where using compression would *           overload the wiki server's resources more than sending *           uncompressed content. * @return this MediaWiki</tt> */	public MediaWiki setUsingCompression(final boolean newValue) { preferenceLock.writeLock.lock; try { useGzip = newValue; } finally { preferenceLock.writeLock.unlock; }		return this; }

// - - - USER LOGIN AND LOGOUT - - -

/**	 * Attempts to log into the wiki that this MediaWiki</tt> represents * using the specified credentials. * 	 * @param user *           The username to use to log in. * @param password *           The password to use to log in. * @return this MediaWiki</tt> * @throws NullPointerException *            if either   or   is	 * * @throws IllegalArgumentException *            if   is the empty string or	 *               is of length 0 * @throws IOException *            if IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API * @throws MediaWiki.MediaWikiException *            if a MediaWiki API error is returned (subtypes thrown:	 *             MediaWiki.LoginFailureException</tt>,	 *             MediaWiki.LoginDelayException</tt>,	 *             MediaWiki.BlockException</tt>,	 *             MediaWiki.UnknownError</tt>) */	public MediaWiki logIn(final String user, final char[] password) throws NullPointerException, IllegalArgumentException, IOException, SAXException, MediaWiki.MediaWikiException { return logIn(user, password, null); }

/**	 * Attempts to log into the wiki that this <tt>MediaWiki</tt> represents * using the specified credentials. * 	 * @param user *           The username to use to log in. * @param password *           The password to use to log in. * @param domain *           The domain to use to log in, or   if not using *           LDAP authentication. * @return this <tt>MediaWiki</tt> * @throws NullPointerException *            if either   or   is	 * * @throws IllegalArgumentException *            if   is the empty string or	 *               is of length 0 * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API * @throws MediaWiki.MediaWikiException *            if a MediaWiki API error is returned (subtypes thrown:	 *             <tt>MediaWiki.LoginFailureException</tt>,	 *             <tt>MediaWiki.LoginDelayException</tt>,	 *             <tt>MediaWiki.BlockException</tt>,	 *             <tt>MediaWiki.UnknownError</tt>) */	public MediaWiki logIn(final String user, final char[] password, final String domain) throws NullPointerException, IllegalArgumentException, IOException, MediaWiki.MediaWikiException { if (user.length == 0) throw new IllegalArgumentException("user is empty"); if (password.length == 0) throw new IllegalArgumentException("password is empty");

Map<String, String> postParams = new TreeMap<String, String>; postParams.put("lgname", user); postParams.put("lgpassword", new String(password)); if (domain != null) { if (password.length == 0) throw new IllegalArgumentException("domain is empty"); postParams.put("lgdomain", domain); }

Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "login"); getParams.put("format", "xml"); String url = createApiGetUrl(getParams);

int retry = 0; networkLock.lock; try { do { InputStream in = post(url, postParams); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList loginTags = xml.getElementsByTagName("login"); if (loginTags.getLength >= 1) { Element loginTag = (Element) loginTags.item(0); String result = loginTag.getAttribute("result"); // Errors not checked for: NoName, EmptyPass, mustbeposted if (result.equals("NeedToken")) { retry++; postParams.put("lgtoken", loginTag.getAttribute("token")); } else if (result.equals("Success")) return this; else if (result.equals("Illegal")) throw new MediaWiki.LoginFailureException("Disallowed username: " + user); else if (result.equals("NotExists")) throw new MediaWiki.LoginFailureException("Inexistent user: " + user); else if (result.equals("WrongPass") || result.equals("WrongPluginPass")) throw new MediaWiki.LoginFailureException("Incorrect password for user: " + user); else if (result.equals("CreateBlocked")) throw new MediaWiki.BlockException("Automatic user creation failed due to an IP block for user: " + user); else if (result.equals("Throttled")) throw new MediaWiki.LoginDelayException(Integer.parseInt(loginTag.getAttribute("wait"))); else if (result.equals("Blocked")) throw new MediaWiki.BlockException(user); else throw new MediaWiki.UnknownError("login: " + result);

}			} while (retry <= 1); } finally { networkLock.unlock; }		throw new MediaWiki.UnknownError("login"); }

/**	 * Logs out from the wiki that this <tt>MediaWiki</tt> represents. * 	 * @return this <tt>MediaWiki</tt> * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki */	public MediaWiki logOut throws IOException { Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "logout"); getParams.put("format", "xml"); String url = createApiGetUrl(getParams);

networkLock.lock; try { get(url); } finally { networkLock.unlock; }

return this; }

/**	 * Returns information about the currently logged-in user on the wiki that * this <tt>MediaWiki</tt> represents. * 	 * @return information about the currently logged-in user on the wiki that *        this <tt>MediaWiki</tt> represents * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API * @throws MediaWiki.MediaWikiException *            if the API does not return a result in the expected format *            (subtypes thrown: <tt>MediaWiki.UnknownError</tt>) */	public MediaWiki.CurrentUser getCurrentUser throws IOException, MediaWiki.MediaWikiException { Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "userinfo"); getParams.put("uiprop", "hasmsg|groups|rights|blockinfo|editcount"); String url = createApiGetUrl(getParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList userinfoTags = xml.getElementsByTagName("userinfo"); if (userinfoTags.getLength >= 1) { Element userinfoTag = (Element) userinfoTags.item(0);

boolean isAnonymous = userinfoTag.hasAttribute("anon"); String userName = userinfoTag.getAttribute("name"); long userID = Long.parseLong(userinfoTag.getAttribute("id")); boolean hasNewMessages = userinfoTag.hasAttribute("messages"); String blockedBy = userinfoTag.hasAttribute("blockedby") ? userinfoTag.getAttribute("blockedby") : null; String blockReason = userinfoTag.hasAttribute("blockreason") ? userinfoTag.getAttribute("blockreason") : null; long editCount = Long.parseLong(userinfoTag.getAttribute("editcount"));

Collection<String> rights = new TreeSet<String>; Element rightsTag = (Element) userinfoTag.getElementsByTagName("rights").item(0); NodeList rTags = rightsTag.getElementsByTagName("r"); for (int i = 0; i < rTags.getLength; i++) rights.add(rTags.item(i).getTextContent);

Collection<String> groups = new TreeSet<String>; Element groupsTag = (Element) userinfoTag.getElementsByTagName("groups").item(0); NodeList gTags = groupsTag.getElementsByTagName("g"); for (int i = 0; i < gTags.getLength; i++) groups.add(gTags.item(i).getTextContent);

return new MediaWiki.CurrentUser(isAnonymous, userName, userID, hasNewMessages, groups, rights, editCount, blockedBy, blockReason); }			throw new MediaWiki.UnknownError("getCurrentUser"); } finally { networkLock.unlock; }	}

// - - - META: SITE INFO - - -

/**	 * A <tt>SoftReference</tt> towards an unmodifiable view of the list of * namespaces available on the wiki that this <tt>MediaWiki</tt> represents. */	private transient SoftReference<MediaWiki.Namespaces> namespaceCache;

/**	 * Gets a list of namespaces on the wiki that this <tt>MediaWiki</tt> * represents. The return value may be cached from an earlier invocation of * the method on the same <tt>MediaWiki</tt>. * 	 * @return a list of namespaces on the wiki that this <tt>MediaWiki</tt> *        represents * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API */	public MediaWiki.Namespaces getNamespaces throws IOException { preferenceLock.readLock.lock; try { MediaWiki.Namespaces result; if (namespaceCache != null && (result = namespaceCache.get) != null) return result; } finally { preferenceLock.readLock.unlock; }		// Not cached. Get, cache and return the result now.

Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "siteinfo"); getParams.put("siprop", "namespaces|namespacealiases"); String url = createApiGetUrl(getParams);

Map<Long, String> canonicalNames = new HashMap<Long, String>; Map<Long, Collection<String>> aliases = new HashMap<Long, Collection<String>>; Map<Long, Boolean> caseSensitives = new HashMap<Long, Boolean>; Map<Long, Boolean> areContent = new HashMap<Long, Boolean>; Map<Long, Boolean> allowSubpages = new HashMap<Long, Boolean>;

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

// Process. NodeList namespacesTags = xml.getElementsByTagName("namespaces");

if (namespacesTags.getLength >= 1) { Element namespacesTag = (Element) namespacesTags.item(0);

NodeList nsTags = namespacesTag.getElementsByTagName("ns"); for (int i = 0; i < nsTags.getLength; i++) { Element nsTag = (Element) nsTags.item(i); long id = Long.parseLong(nsTag.getAttribute("id")); Long lID = Long.valueOf(id); canonicalNames.put(lID, nsTag.getAttribute("canonical")); if (nsTag.getChildNodes.getLength > 0 && !nsTag.getTextContent.equals(nsTag.getAttribute("canonical"))) { // Add the content of <ns> to aliases. Collection<String> aliasesForNamespace = new TreeSet<String>; aliasesForNamespace.add(nsTag.getTextContent); aliases.put(lID, aliasesForNamespace); }					caseSensitives.put(lID, nsTag.getAttribute("case").equals("case-sensitive")); areContent.put(lID, nsTag.hasAttribute("content")); allowSubpages.put(lID, nsTag.hasAttribute("subpages")); }			}

// Process. NodeList namespaceAliasesTags = xml.getElementsByTagName("namespacealiases");

if (namespaceAliasesTags.getLength >= 1) { Element namespaceAliasesTag = (Element) namespaceAliasesTags.item(0);

NodeList nsTags = namespaceAliasesTag.getElementsByTagName("ns"); for (int i = 0; i < nsTags.getLength; i++) { Element nsTag = (Element) nsTags.item(i); long id = Long.parseLong(nsTag.getAttribute("id")); Long lID = Long.valueOf(id); if (nsTag.getChildNodes.getLength > 0) { // Add the content of <ns> to aliases. Collection<String> aliasesForNamespace = aliases.get(lID); if (aliasesForNamespace == null) { aliasesForNamespace = new TreeSet<String>; aliases.put(lID, aliasesForNamespace); }						aliasesForNamespace.add(nsTag.getTextContent); }				}			}

Collection<MediaWiki.Namespace> namespaces = new ArrayList<Namespace>(canonicalNames.size); for (Long id : canonicalNames.keySet) namespaces.add(new MediaWiki.Namespace(id, canonicalNames.get(id), aliases.get(id), caseSensitives.get(id), areContent.get(id), allowSubpages.get(id))); MediaWiki.Namespaces result = new MediaWiki.Namespaces(namespaces); preferenceLock.writeLock.lock; try { namespaceCache = new SoftReference<MediaWiki.Namespaces>(result); } finally { preferenceLock.writeLock.unlock; }			return result; } finally { networkLock.unlock; }	}

/**	 * A <tt>SoftReference</tt> towards an unmodifiable view of the list of	 * interwiki prefixes available on the wiki that this <tt>MediaWiki</tt> * represents. */	private transient SoftReference<MediaWiki.InterwikiPrefixes> interwikiPrefixCache;

/**	 * Gets a list of interwiki prefixes on the wiki that this * <tt>MediaWiki</tt> represents. The return value may be cached from an * earlier invocation of the method on the same <tt>MediaWiki</tt>. * 	 * @return a list of interwiki prefixes on the wiki that this *        <tt>MediaWiki</tt> represents * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API */	public MediaWiki.InterwikiPrefixes getInterwikiPrefixes throws IOException { preferenceLock.readLock.lock; try { MediaWiki.InterwikiPrefixes result; if (interwikiPrefixCache != null && (result = interwikiPrefixCache.get) != null) return result; } finally { preferenceLock.readLock.unlock; }		// Not cached. Get, cache and return the result now.

Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "siteinfo"); getParams.put("siprop", "interwikimap"); String url = createApiGetUrl(getParams);

Map<String, Boolean> areLocal = new HashMap<String, Boolean>; Map<String, String> urlPatterns = new HashMap<String, String>; Map<String, String> languages = new HashMap<String, String>;

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList interwikiMapTags = xml.getElementsByTagName("interwikimap");

if (interwikiMapTags.getLength >= 1) { Element interwikiMapTag = (Element) interwikiMapTags.item(0);

NodeList iwTags = interwikiMapTag.getElementsByTagName("iw"); for (int i = 0; i < iwTags.getLength; i++) { Element iwTag = (Element) iwTags.item(i); String name = iwTag.getAttribute("prefix"); urlPatterns.put(name, iwTag.getAttribute("url")); if (iwTag.hasAttribute("language")) { languages.put(name, iwTag.getAttribute("language")); }					areLocal.put(name, iwTag.hasAttribute("local")); }			}

Map<String, MediaWiki.InterwikiPrefix> interwikiPrefixes = new TreeMap<String, InterwikiPrefix>; for (String name : urlPatterns.keySet) interwikiPrefixes.put(name, new MediaWiki.InterwikiPrefix(name, languages.get(name), urlPatterns.get(name), areLocal.get(name))); MediaWiki.InterwikiPrefixes result = new MediaWiki.InterwikiPrefixes(interwikiPrefixes); preferenceLock.writeLock.lock; try { interwikiPrefixCache = new SoftReference<MediaWiki.InterwikiPrefixes>(result); } finally { preferenceLock.writeLock.unlock; }			return result; } finally { networkLock.unlock; }	}

/**	 * Gets various statistics about the wiki that this <tt>MediaWiki</tt> * represents. * 	 * @return an object containing various statistics on the wiki that this *        <tt>MediaWiki</tt> represents * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API * @throws MediaWiki.MediaWikiException *            if the API does not return a result in the expected format *            (subtypes thrown: <tt>MediaWiki.UnknownError</tt>) */	public MediaWiki.Statistics getStatistics throws IOException, MediaWiki.MediaWikiException { Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "siteinfo"); getParams.put("siprop", "statistics"); String url = createApiGetUrl(getParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList statisticsTags = xml.getElementsByTagName("statistics");

if (statisticsTags.getLength >= 1) { Element statisticsTag = (Element) statisticsTags.item(0);

long pages = Long.parseLong(statisticsTag.getAttribute("pages")); long articles = Long.parseLong(statisticsTag.getAttribute("articles")); long edits = Long.parseLong(statisticsTag.getAttribute("edits")); long images = Long.parseLong(statisticsTag.getAttribute("images")); long users = Long.parseLong(statisticsTag.getAttribute("users")); long activeUsers = Long.parseLong(statisticsTag.getAttribute("activeusers")); long admins = Long.parseLong(statisticsTag.getAttribute("admins")); long jobs = Long.parseLong(statisticsTag.getAttribute("jobs")); long views = -1; if (statisticsTag.hasAttribute("views")) views = Long.parseLong(statisticsTag.getAttribute("views")); return new MediaWiki.Statistics(pages, articles, edits, images, users, activeUsers, admins, jobs, views); }			throw new MediaWiki.UnknownError("statistics"); } finally { networkLock.unlock; }	}

/**	 * Returns the database replication lag on the wiki that this * <tt>MediaWiki</tt> represents, in seconds. According to the API * documentation, a lag of over 5 seconds indicates that scripts should stop * making large amounts of edits to let the database replicas catch up. * 	 * The return value is  if, for some reason, * there is no database server named in the reply from the MediaWiki API. * This may indicate that all database replicas are down. * 	 * @return the database replication lag on the wiki that this *        <tt>MediaWiki</tt> represents, in seconds * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API */	public long getDatabaseReplicationLag throws IOException { Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "siteinfo"); getParams.put("siprop", "dbrepllag"); String url = createApiGetUrl(getParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList dbReplLagTags = xml.getElementsByTagName("dbrepllag"); if (dbReplLagTags.getLength >= 1) { Element dbReplLagTag = (Element) dbReplLagTags.item(0); NodeList dbTags = dbReplLagTag.getElementsByTagName("db");

if (dbTags.getLength == 0) return Long.MAX_VALUE;

long maxLag = 0;

for (int i = 0; i < dbTags.getLength; i++) { Element dbTag = (Element) dbTags.item(i); long lag = Long.parseLong(dbTag.getAttribute("lag")); maxLag = Math.max(maxLag, lag); }

return maxLag; } else return Long.MAX_VALUE; } finally { networkLock.unlock; }	}

/**	 * A <tt>SoftReference</tt> towards an unmodifiable view of the list of	 * special pages available on the wiki that this <tt>MediaWiki</tt> * represents. */	private transient SoftReference<Map<String, String>> specialPageAliasCache;

/**	 * Gets a list of special page aliases on the wiki that this * <tt>MediaWiki</tt> represents. The return value may be cached from an * earlier invocation of the method on the same <tt>MediaWiki</tt>. * 	 * @return a list of special page aliases on the wiki that this *        <tt>MediaWiki</tt> represents * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or while reading the XML reply from the API */	public Map<String, String> getSpecialPageAliases throws IOException { preferenceLock.readLock.lock; try { Map<String, String> result; if (specialPageAliasCache != null && (result = specialPageAliasCache.get) != null) return result; } finally { preferenceLock.readLock.unlock; }		// Not cached. Get, cache and return the result now.

Map<String, String> getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("meta", "siteinfo"); getParams.put("siprop", "specialpagealiases"); String url = createApiGetUrl(getParams);

Map<String, String> result = new HashMap<String, String>;

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList specialPageTags = xml.getElementsByTagName("specialpage");

for (int i = 0; i < specialPageTags.getLength; i++) { Element specialPageTag = (Element) specialPageTags.item(i);

String realName = specialPageTag.getAttribute("realname");

NodeList aliasTags = specialPageTag.getElementsByTagName("alias"); for (int j = 0; j < aliasTags.getLength; j++) { Element aliasTag = (Element) aliasTags.item(j);

String alias = aliasTag.getTextContent; result.put(alias, realName); }

result.put(realName, realName); }

preferenceLock.writeLock.lock; try { specialPageAliasCache = new SoftReference<Map<String, String>>(result); } finally { preferenceLock.writeLock.unlock; }			return result; } finally { networkLock.unlock; }	}

// - - - PAGE INFORMATION (PROP=INFO) - - -

/**	 * Retrieves information about pages specified by their full names. The * return value is an iterator which will return information about the pages * in the  parameter in order when its * method is called. The iterator's  method may: * <ul> * <li>return, if it encounters a missing page; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param titles *           The full name(s) of the page(s) to return information about. * @return an iterator which will return information about the pages in the *          parameter in order when its *        method is called */	public Iterator<MediaWiki.PageInformation> getPageInformationForTitles(String... titles) { return new MediaWiki.PageInformationIterator("titles", titles); }

/**	 * Retrieves information about pages specified by their IDs. The return * value is an iterator which will return information about the pages in the *  parameter in order when its   method is * called. The iterator's  method may: * <ul> * <li>return, if it encounters a missing page; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param ids *           The ID(s) of the page(s) to return information about. * @return an iterator which will return information about the pages in the *          parameter in order when its *        method is called */	public Iterator<MediaWiki.PageInformation> getPageInformationForPageIDs(long... ids) { return new MediaWiki.PageInformationIterator("pageids", ids); }

/**	 * Retrieves information about the pages whose history contains the * specified revision(s). The return value is an iterator which will return * information about the pages whose history contains each entry in the *  parameter in order when its   method is * called. The iterator's  method may: * <ul> * <li>return, if it encounters a missing revision; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param ids *           The ID(s) of the revision(s) whose pages are to be examined. * @return an iterator which will return information about the pages whose *        history contains each entry in the   parameter in	 *         order when its   method is called */	public Iterator<MediaWiki.PageInformation> getPageInformationForRevisionIDs(long... ids) { return new MediaWiki.PageInformationIterator("revids", ids); }

private static final SimpleDateFormat iso8601TimestampParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);

static { iso8601TimestampParser.setTimeZone(TimeZone.getTimeZone("GMT")); }

private class PageInformationIterator implements Iterator<MediaWiki.PageInformation> { /**		 * The type of the elements given. This may be "titles", "revids" or		 * "pageids", corresponding to the parameter names acceptable for * prop=info. */		private final String elementType;

/**		 * The elements (titles, revision IDs or page IDs) to get information * about. The type of this field is either String[] or long[]. */		private final Object elements;

private int i;

private final Map<String, String> getParams;

PageInformationIterator(final String elementType, final Object elements) { if (elementType == null || elementType.length == 0) throw new IllegalArgumentException("elementType is null or empty"); if (!(elements instanceof String[] || elements instanceof long[])) throw new IllegalArgumentException("elements does not have an acceptable type"); this.elementType = elementType; this.elements = elements;

getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("prop", "info"); getParams.put("inprop", "protection");

i = -1; }

@Override public synchronized boolean hasNext { return i + 1 < (elements instanceof String[] ? ((String[]) elements).length : ((long[]) elements).length); }

@Override public synchronized MediaWiki.PageInformation next throws MediaWiki.IterationException { i++;

getParams.put(elementType, elements instanceof String[] ? ((String[]) elements)[i] : Long.toString(((long[]) elements)[i]));

String url = createApiGetUrl(getParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList pageTags = xml.getElementsByTagName("page");

if (pageTags.getLength > 0) { Element pageTag = (Element) pageTags.item(0);

String title = pageTag.getAttribute("title"); boolean missing = pageTag.hasAttribute("missing"); long namespaceID = Long.parseLong(pageTag.getAttribute("ns"));

long pageID = pageTag.hasAttribute("pageid") ? Long.parseLong(pageTag.getAttribute("pageid")) : -1; Date lastEdit = pageTag.hasAttribute("touched") ? iso8601TimestampParser.parse(pageTag.getAttribute("touched")) : null; long lastRevisionID = pageTag.hasAttribute("lastrevid") ? Long.parseLong(pageTag.getAttribute("lastrevid")) : -1; long views = pageTag.hasAttribute("counter") ? Long.parseLong(pageTag.getAttribute("counter")) : -1; long length = pageTag.hasAttribute("length") ? Long.parseLong(pageTag.getAttribute("length")) : 0; boolean isRedirect = pageTag.hasAttribute("redirect"); boolean isNew = pageTag.hasAttribute("new");

Map<String, MediaWiki.Protection> protections = new TreeMap<String, Protection>;

NodeList prTags = pageTag.getElementsByTagName("pr");

for (int i = 0; i < prTags.getLength; i++) { Element prTag = (Element) prTags.item(0);

String type = prTag.getAttribute("type"); String level = prTag.getAttribute("level"); Date expiry = prTag.getAttribute("expiry").equals("infinity") ? null : iso8601TimestampParser.parse(prTag.getAttribute("expiry")); boolean isCascading = prTag.hasAttribute("cascade"); String cascadeSource = prTag.hasAttribute("source") ? prTag.getAttribute("source") : null;

protections.put(type, new MediaWiki.Protection(level, expiry, isCascading, cascadeSource)); }

return new MediaWiki.PageInformation(missing, pageID, title, lastEdit, namespaceID, lastRevisionID, views, length, isRedirect, isNew, protections); }

throw new MediaWiki.IterationException("no information returned about the next page"); } catch (IOException ioe) { throw new MediaWiki.IterationException(ioe); } catch (ParseException pe) { throw new MediaWiki.IterationException(pe); } finally { networkLock.unlock; }		}

@Override public void remove { throw new UnsupportedOperationException; }	}

// - - - REVISION INFORMATION (PROP=REVISIONS) - - -

/**	 * Retrieves information about the last revision of pages specified by their * full names. The return value is an iterator which will return information * about the last revision of the pages listed in the * parameter in order when its  method is called. The * iterator's  method may: * <ul> * <li>return, if it encounters a missing page; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param titles *           The full name(s) of the pages(s) to return information about *           the last revision of. * @return an iterator which will return information about the last revision *        of the pages listed in the   parameter in order *        when its   method is called */	public Iterator<MediaWiki.RevisionInformation> getLastRevisionForTitles(String... titles) { return new MediaWiki.SingleRevisionIterator("titles", titles); }

/**	 * Retrieves information about the last revision of pages specified by their * ID. The return value is an iterator which will return information about * the last revision of the pages listed in the  parameter * in order when its  method is called. The iterator's	 *  method may: * <ul> * <li>return, if it encounters a missing page; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param ids *           The ID(s) of the pages(s) to return information about the last *           revision of. * @return an iterator which will return information about the last revision *        of the pages listed in the   parameter in order *        when its   method is called */	public Iterator<MediaWiki.RevisionInformation> getLastRevisionForPageIDs(long... ids) { return new MediaWiki.SingleRevisionIterator("pageids", ids); }

/**	 * Retrieves information about the specified revision(s). The return value * is an iterator which will return information about each revision of the *  parameter in order when its   method is * called. The iterator's  method may: * <ul> * <li>return, if it encounters a missing revision; * <li>throw <tt>MediaWiki.IterationException</tt>, an unchecked exception, * if it encounters an error. * </ul> * 	 * @param ids *           The ID(s) of the revision(s) to return information about. * @return an iterator which will return information about each revision of	 *        the   parameter in order when its *          method is called */	public Iterator<MediaWiki.RevisionInformation> getRevisions(long... ids) { return new MediaWiki.SingleRevisionIterator("revids", ids); }

private class SingleRevisionIterator implements Iterator<MediaWiki.RevisionInformation> { /**		 * The type of the elements given. This may be "titles", "revids" or		 * "pageids", corresponding to the parameter names acceptable for * prop=revisions. */		private final String elementType;

/**		 * The elements (titles, revision IDs or page IDs) to get information * about. The type of this field is either String[] or long[]. */		private final Object elements;

private int i;

private final Map<String, String> getParams;

SingleRevisionIterator(final String elementType, final Object elements) { if (elementType == null || elementType.length == 0) throw new IllegalArgumentException("elementType is null or empty"); if (!(elements instanceof String[] || elements instanceof long[])) throw new IllegalArgumentException("elements does not have an acceptable type"); this.elementType = elementType; this.elements = elements;

getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("prop", "revisions"); getParams.put("rvprop", "ids|flags|timestamp|user|comment|size");

i = -1; }

@Override public synchronized boolean hasNext { return i + 1 < (elements instanceof String[] ? ((String[]) elements).length : ((long[]) elements).length); }

@Override public synchronized MediaWiki.RevisionInformation next throws MediaWiki.IterationException { i++;

getParams.put(elementType, elements instanceof String[] ? ((String[]) elements)[i] : Long.toString(((long[]) elements)[i]));

String url = createApiGetUrl(getParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList badrevidsTags = xml.getElementsByTagName("badrevids"); if (badrevidsTags.getLength > 0) return null;

NodeList pageTags = xml.getElementsByTagName("page");

if (pageTags.getLength > 0) { Element pageTag = (Element) pageTags.item(0);

if (pageTag.hasAttribute("missing")) return null;

NodeList revTags = pageTag.getElementsByTagName("rev");

if (revTags.getLength > 0) { Element revTag = (Element) revTags.item(0);

long revisionID = Long.parseLong(revTag.getAttribute("revid")); long parentID = Long.parseLong(revTag.getAttribute("parentid"));

Date timestamp = iso8601TimestampParser.parse(revTag.getAttribute("timestamp"));

boolean userHidden = revTag.hasAttribute("userhidden"); String userName = userHidden ? null : revTag.getAttribute("user");

boolean commentHidden = revTag.hasAttribute("commenthidden"); String comment = commentHidden ? null : revTag.getAttribute("comment");

boolean isMinor = revTag.hasAttribute("minor"); boolean isAnonymous = revTag.hasAttribute("anon");

long length = revTag.hasAttribute("size") ? Long.parseLong(revTag.getAttribute("size")) : 0;

return new MediaWiki.RevisionInformation(revisionID, parentID, timestamp, userName, userHidden, length, comment, commentHidden, isMinor, isAnonymous); }				}

throw new MediaWiki.IterationException("no information returned about the next revision"); } catch (IOException ioe) { throw new MediaWiki.IterationException(ioe); } catch (ParseException pe) { throw new MediaWiki.IterationException(pe); } finally { networkLock.unlock; }		}

@Override public void remove { throw new UnsupportedOperationException; }	}

public Iterator<MediaWiki.RevisionInformation> getRevisionsForTitle(String title) { return getRevisionsForTitle(title, (Date) null, (Date) null); }

public Iterator<MediaWiki.RevisionInformation> getRevisionsForTitle(String title, Date start, Date end) { return new MediaWiki.MultipleRevisionIterator("titles", title, "rvstart", start, "rvend", end); }

public Iterator<MediaWiki.RevisionInformation> getRevisionsForTitle(String title, Long startID, Long endID) { return new MediaWiki.MultipleRevisionIterator("titles", title, "rvstartid", startID, "rvendid", endID); }

public Iterator<MediaWiki.RevisionInformation> getRevisionsForPageID(long id) { return getRevisionsForPageID(id, (Date) null, (Date) null); }

public Iterator<MediaWiki.RevisionInformation> getRevisionsForPageID(long id, Date start, Date end) { return new MediaWiki.MultipleRevisionIterator("pageids", id, "rvstart", start, "rvend", end); }

public Iterator<MediaWiki.RevisionInformation> getRevisionsForPageID(long id, Long startID, Long endID) { return new MediaWiki.MultipleRevisionIterator("pageids", id, "rvstartid", startID, "rvendid", endID); }

private class MultipleRevisionIterator implements Iterator<MediaWiki.RevisionInformation> { /**		 * The type of the element given for the lower bound. This is either * "rvstart" or "rvstartid", corresponding to the parameter names * acceptable for prop=revisions enumeration. */		private String startType;

/**		 * The element given for the lower bound. The type of this field is		 * either Date or Long, and may be  if the start * parameter is to be omitted. */		private Object start;

/**		 * The index of the last revision tag returned by this <tt>Iterator</tt> * 's  method among the tags in  , * or -1 if no revision tag was parsed yet in. */		private int i;

/**		 * List of nodes returned by the MediaWiki API for this iteration and * which are being enumerated. Query continuation transparently loads * more nodes into this field. */		private List<Element> upcoming;

private final Map<String, String> getParams;

MultipleRevisionIterator(final String elementType, final Object element, final String startType, final Object start, final String endType, final Object end) { if (elementType == null || elementType.length == 0) throw new IllegalArgumentException("elementType is null or empty"); if (!(element instanceof String || element instanceof Long)) throw new IllegalArgumentException("element does not have an acceptable type");

getParams = new TreeMap<String, String>; getParams.put("action", "query"); getParams.put("format", "xml"); getParams.put("prop", "revisions"); // &{titles | revids | pageids}= getParams.put(elementType, element.toString); getParams.put("rvprop", "ids|flags|timestamp|user|comment|size"); getParams.put("rvdir", "newer"); getParams.put("rvlimit", "max"); this.startType = startType; this.start = start; if (end != null) { getParams.put(endType, end.toString); }

i = -1; }

@Override public synchronized boolean hasNext throws MediaWiki.IterationException { cacheUpcoming; return i + 1 < upcoming.size; }

@Override public synchronized MediaWiki.RevisionInformation next throws MediaWiki.IterationException { cacheUpcoming;

try { Element revTag = upcoming.get(++i);

long revisionID = Long.parseLong(revTag.getAttribute("revid")); long parentID = Long.parseLong(revTag.getAttribute("parentid"));

Date timestamp = iso8601TimestampParser.parse(revTag.getAttribute("timestamp"));

boolean userHidden = revTag.hasAttribute("userhidden"); String userName = userHidden ? null : revTag.getAttribute("user");

boolean commentHidden = revTag.hasAttribute("commenthidden"); String comment = commentHidden ? null : revTag.getAttribute("comment");

boolean isMinor = revTag.hasAttribute("minor"); boolean isAnonymous = revTag.hasAttribute("anon");

long length = revTag.hasAttribute("size") ? Long.parseLong(revTag.getAttribute("size")) : 0;

return new MediaWiki.RevisionInformation(revisionID, parentID, timestamp, userName, userHidden, length, comment, commentHidden, isMinor, isAnonymous); } catch (ParseException pe) { throw new MediaWiki.IterationException(pe); } finally { if (i + 1 >= upcoming.size) { upcoming = null; // The next call will fill up upcoming i = -1; }			}		}

protected synchronized void cacheUpcoming { if (upcoming == null) { if (startType.equals("done")) { upcoming = Collections.emptyList; return; }				// Get the next page of revisions from the API. // The query continue value from the previous call will be in // start, if applicable. Map<String, String> pageGetParams = new TreeMap<String, String>(getParams); if (start != null) { // The start parameter is for only this get. pageGetParams.put(startType, start instanceof Date ? dateToTimestamp((Date) start) : start.toString); }

String url = createApiGetUrl(pageGetParams);

networkLock.lock; try { InputStream in = get(url); Document xml; try { xml = documentBuilder.parse(in); } catch (SAXException e) { throw new IOException(e); }

NodeList badrevidsTags = xml.getElementsByTagName("badrevids"); if (badrevidsTags.getLength > 0) upcoming = Collections.emptyList; else { NodeList pageTags = xml.getElementsByTagName("page");

if (pageTags.getLength > 0) { Element pageTag = (Element) pageTags.item(0);

if (pageTag.hasAttribute("missing")) upcoming = Collections.emptyList; else { upcoming = new ArrayList<Element>; NodeList revTags = pageTag.getElementsByTagName("rev");

for (int i = 0; i < revTags.getLength; i++) { upcoming.add((Element) revTags.item(i)); }							}						}

/*						 * Honor query-continue: start at the next named ID.If * there's no query-continue, the iteration is done. Put * the special value "done" in startType so that the * next call doesn't continue the iteration. */						NodeList queryContinueTags = xml.getElementsByTagName("query-continue");

if (queryContinueTags.getLength > 0) { Element queryContinueTag = (Element) queryContinueTags.item(0);

NodeList revisionsTags = queryContinueTag.getElementsByTagName("revisions");

if (revisionsTags.getLength > 0) { Element revisionsTag = (Element) revisionsTags.item(0);

// Whatever the initial start type was, it's now // by ID. startType = "rvstartid"; start = Long.valueOf(revisionsTag.getAttribute("rvstartid")); } else startType = "done"; } else startType = "done"; }				} catch (IOException ioe) { throw new MediaWiki.IterationException(ioe); } finally { networkLock.unlock; }			}

}

@Override public void remove { throw new UnsupportedOperationException; }	}

// - - - DATA CLASSES - - -

public static class CurrentUser { private final boolean isAnonymous;

private final long userID;

private final String userName;

private final boolean hasNewMessages;

private final Set<String> groups;

private final Set<String> rights;

private final long editCount;

private final String blockingUser;

private final String blockReason;

CurrentUser(final boolean isAnonymous, final String userName, final long userID, final boolean hasNewMessages, final Collection<String> groups, final Collection<String> rights, final long editCount, final String blockingUser, final String blockReason) { this.userID = userID; this.userName = userName; this.isAnonymous = isAnonymous; this.hasNewMessages = hasNewMessages; this.groups = Collections.unmodifiableSet(groups instanceof TreeSet<?> ? (TreeSet<String>) groups : new TreeSet<String>(groups)); this.rights = Collections.unmodifiableSet(rights instanceof TreeSet<?> ? (TreeSet<String>) rights : new TreeSet<String>(rights)); this.editCount = editCount; this.blockingUser = blockingUser; this.blockReason = blockReason; }

/**		 * Return whether the currently logged-in user is an anonymous user, * i.e. an IP address. * 		 * @return whether the currently logged-in user is an anonymous user */		public boolean isAnonymous { return isAnonymous; }

/**		 * Returns the ID of the currently logged-in user. The return value is * undefined if  returns. * 		 * @return the ID of the currently logged-in user */		public long getUserID { return userID; }

/**		 * Returns the username (not real name) of the currently logged-in user. * The return value is an IP address if * returns. * 		 * @return the username of the currently logged-in user */		public String getUserName { return userName; }

/**		 * Returns whether the currently logged-in user has new messages. * 		 * @return whether the currently logged-in user has new messages */		public boolean hasNewMessages { return hasNewMessages; }

/**		 * Returns an immutable view of the list of groups that the currently * logged-in user is a part of. * 		 * @return an immutable view of the list of groups that the currently *        logged-in user is a part of		 */ public Collection<String> getGroups { return groups; }

/**		 * Returns an immutable view of the list of rights that the currently * logged-in user has. * 		 * @return an immutable view of the list of rights that the currently *        logged-in user has */		public Collection<String> getRights { return rights; }

/**		 * Returns whether the currently logged-in user is in all of the * specified groups. The return value is  if no groups * are specified. * 		 * @param groups *           The groups to check membership in. * @return whether the currently logged-in user is in all of the *        specified groups */		public boolean isInGroups(String... groups) { for (String group : groups) { if (!this.groups.contains(group)) return false; }			return true; }

/**		 * Returns whether the currently logged-in user has all of the specified * rights. The return value is  if no rights are * specified. * 		 * @param rights *           The rights to check possession of. * @return whether the currently logged-in user has all of the specified *        rights */		public boolean hasRights(String... rights) { for (String right : rights) { if (!this.rights.contains(right)) return false; }			return true; }

/**		 * Returns the number of edits made by the currently logged-in user on * the wiki represented by the enclosing <tt>MediaWiki</tt>. * 		 * @return the number of edits made by the currently logged-in user on		 *        the wiki represented by the enclosing <tt>MediaWiki</tt> */		public long getEditCount { return editCount; }

/**		 * Returns the name of the user who blocked the currently logged-in * user. The return value is  if the currently * logged-in user is not blocked. * 		 * @return the name of the user who blocked the currently logged-in user */		public String getBlockingUser { return blockingUser; }

/**		 * Returns the reason why the currently logged-in user is blocked. The * return value is  if the currently logged-in user is * not blocked. * 		 * @return the reason why the currently logged-in user is blocked */		public String getBlockReason { return blockReason; }

@Override public String toString { // CurrentUser["Name" *new messages* (user ID X) (anonymous), // groups, rights, Y edits, blocked by Sysop (Reason)] return String.format("CurrentUser[\"%s\"%s (user ID %d)%s, groups: %s, %rights: %s, %d edits%s]", userName, hasNewMessages ? " *new messages*" : "", userID, isAnonymous ? " (anonymous)" : "", groups, rights, editCount, blockingUser != null ? ", blocked by " + blockingUser + " (" + blockReason + ")" : ""); }	}

public static class Namespaces { private final Collection<MediaWiki.Namespace> list;

Namespaces(final Collection<MediaWiki.Namespace> list) { this.list = Collections.unmodifiableCollection(list); }

/**		 * Returns a <tt>Namespace</tt> object matching the given *  drawn from this <tt>Namespaces</tt>'s list. The * return value is  if there is no such namespace. * 		 * @param prefix *           The prefix to retrieve a <tt>Namespace</tt> for. This *           value may not end with a colon , nor may *           it contain a page name. For example, *            .		 * @return a <tt>Namespace</tt> object matching the given *         		 */		public MediaWiki.Namespace getNamespace(final String prefix) { for (MediaWiki.Namespace namespace : list) if (namespace.matches(prefix)) return namespace; return null; }

/**		 * Returns a <tt>Namespace</tt> object having the given * drawn from this <tt>Namespaces</tt>'s list. The return value is *  if there is no such namespace. * 		 * @param id *           The id of the <tt>Namespace</tt> to retrieve. * @return a <tt>Namespace</tt> object matching the given *         		 */		public MediaWiki.Namespace getNamespace(final long id) { for (MediaWiki.Namespace namespace : list) if (namespace.getID == id) return namespace; return null; }

/**		 * Returns the <tt>Namespace</tt> object designating the namespage * containing the page of the given name drawn from this * <tt>Namespaces</tt>'s list. The return value is the * <tt>Namespace</tt> object having the ID of 0, i.e. the main * namespace, if there is no such namespace. * 		 * @param fullPageName *           The page to retrieve the <tt>Namespace</tt> of. For *           example, "Paperclip" (in the main namespace), *           "Project:Administrator requests" (in the Project		 *            namespace), "Inexistent namespace:Example" (in the main		 *            namespace, despite there being a colon in the full page		 *            name). * @return the <tt>Namespace</tt> object designating the namespage *        containing the page of the given name */		public MediaWiki.Namespace getNamespaceForPage(final String fullPageName) { int colonIndex = fullPageName.indexOf(':'); if (colonIndex == -1) return getNamespace(0L); MediaWiki.Namespace result; return (result = getNamespace(fullPageName.substring(0, colonIndex))) != null ? result : getNamespace(0L); }

/**		 * Returns the name of the given page after stripping it of its * namespace prefix. The return value is the page name itself if what is		 * before the colon is not a valid namespace for the wiki that the * enclosing <tt>MediaWiki</tt> represents. * 		 * @param fullPageName *           The page name to strip of its namespace prefix. For *           example, "Paperclip" (in the main namespace) becomes *           "Paperclip", "Project:Administrator requests" (in the		 *            Project namespace) becomes "Administrator requests", *           "Inexistent namespace:Example" (in the main namespace,		 *            despite there being a colon in the full page name) becomes *           "Inexistent namespace:Example". * @return the name of the given page after stripping it of its *        namespace prefix */		public String removeNamespacePrefix(final String fullPageName) { int colonIndex = fullPageName.indexOf(':'); if (colonIndex == -1) return fullPageName; return getNamespace(fullPageName.substring(0, colonIndex)) != null ? fullPageName.substring(colonIndex + 1) : fullPageName; }

/**		 * Returns the name of the given page's talk page. * 		 * @param fullPageName *           The page name to get the talk page of. * @return the name of the given page's talk page * @throws IllegalArgumentException *            if the given page's namespace does not support having, or		 *             does not have, a talk namespace; if the given page's		 *             namespace is already a talk namespace */		public String getTalkPage(final String fullPageName) throws IllegalArgumentException { MediaWiki.Namespace namespace = getNamespaceForPage(fullPageName); String basePageName = removeNamespacePrefix(fullPageName); if (namespace.getID < 0 /*- Special:, Media: */) throw new IllegalArgumentException("Namespace '" + namespace.getCanonicalName + "' does not support having an associated talk namespace"); else if (namespace.getID % 2 == 1 /*- Talk namespace */) throw new IllegalArgumentException("Namespace '" + namespace.getCanonicalName + "' is a talk namespace"); else { // Talk namespaces have the same ID as the ones they are talking // about, plus 1. MediaWiki.Namespace talkNamespace = getNamespace(namespace.getID + 1L); if (talkNamespace == null) throw new IllegalArgumentException("Namespace '" + namespace.getCanonicalName + "' does not have an associated talk namespace"); return talkNamespace.getFullPageName(basePageName); }		}

@Override public String toString { return list.toString; }	}

public static class Namespace { private final long id;

private final String canonicalName;

private final Set<String> aliases;

private final boolean caseSensitive;

private final boolean isContent;

private final boolean allowsSubpages;

Namespace(final long id, final String canonicalName, final Collection<String> aliases, final boolean caseSensitive, final boolean isContent, final boolean allowsSubpages) { this.id = id; this.canonicalName = canonicalName; this.aliases = Collections.unmodifiableSet(aliases instanceof TreeSet<?> ? (TreeSet<String>) aliases : (aliases != null ? new TreeSet<String>(aliases) : new TreeSet<String>)); this.caseSensitive = caseSensitive; this.isContent = isContent; this.allowsSubpages = allowsSubpages; }

/**		 * Returns the ID of this <tt>Namespace</tt>. * 		 * @return the ID of this <tt>Namespace</tt> */		public long getID { return id; }

/**		 * Returns the canonical name of this <tt>Namespace</t>. * 		 * @return the canonical name of this <tt>Namespace</t> */		public String getCanonicalName { return canonicalName; }

/**		 * Returns an unmodifiable view of the aliases that can be used to refer * to this <tt>Namespace</tt>. * 		 * @return an unmodifiable view of the aliases that can be used to refer *        to this <tt>Namespace</tt> */		public Collection<String> getAliases { return aliases; }

/**		 * Returns whether the first letter of the name of this * <tt>Namespace</tt> must be written with the correct case, as * specified in its canonical name. If, this * <tt>Namespace</tt> allows using any case for its first letter. * 		 * @return whether the first letter of the name of this *        <tt>Namespace</tt> must be written with the correct case, as		 *         specified in its canonical name */		public boolean isCaseSensitive { return caseSensitive; }

/**		 * Returns whether this <tt>Namespace</tt> is a content namespace, i.e. * whether it gets added to page count statistics etc.		 * * @return whether this <tt>Namespace</tt> is a content namespace */		public boolean isContent { return isContent; }

/**		 * Returns whether this <tt>Namespace</tt> allows pages to be under * other pages, separated by slashes. * 		 * @return whether this <tt>Namespace</tt> allows pages to be under *        other pages */		public boolean allowsSubpages { return allowsSubpages; }

/**		 * Returns whether the given  matches this * <tt>Namespace</tt>'s prefix. * 		 * @param prefix *           The prefix to check. This value may not end with a colon *           , nor may it contain a page name. * @return whether the given  matches this *        <tt>Namespace</tt>'s prefix */		public boolean matches(final String prefix) { if (matches(canonicalName, prefix, caseSensitive)) return true; for (String alias : aliases) if (matches(alias, prefix, caseSensitive)) return true; return false; }

/**		 * Returns the full name of a page having the given base name in this * <tt>Namespace</tt>. * 		 * @param basePageName *           Base name of the page, without a namespace prefix or *           colon. * @return the full name of a page having the given base name in this *        <tt>Namespace</tt> */		public String getFullPageName(final String basePageName) { return canonicalName.length != 0 ? canonicalName + ":" + basePageName : basePageName; }

private static boolean matches(final String namespace, final String prefix, final boolean caseSensitive) { if (caseSensitive) // May throw NullPointerException return prefix.equals(namespace); else { // May throw NullPointerException if (namespace.length == 0) return prefix.length == 0; else if (namespace.length == 1) return prefix.length == 1 && Character.toLowerCase(namespace.charAt(0)) == Character.toLowerCase(prefix.charAt(0)); else { return prefix.length == namespace.length && Character.toLowerCase(namespace.charAt(0)) == Character.toLowerCase(prefix.charAt(0)) && namespace.substring(1).replace(' ', '_').equals(prefix.substring(1).replace(' ', '_')); }			}		}

@Override public String toString { return String.format("Namespace[\"%s\" or %s (ID %d), case sensitive: %s, content: %s, subpages allowed: %s]", canonicalName, aliases, id, caseSensitive, isContent, allowsSubpages); }	}

public static class Statistics { private final long pages, articles, edits, images, users, activeUsers, admins, jobs, views;

Statistics(final long pages, final long articles, final long edits, final long images, final long users, final long activeUsers, final long admins, final long jobs, final long views) { this.pages = pages; this.articles = articles; this.edits = edits; this.images = images; this.users = users; this.activeUsers = activeUsers; this.admins = admins; this.jobs = jobs; this.views = views; }

/**		 * Returns the number of pages on all namespaces of the wiki for which * this <tt>Statistics</tt> was created. * 		 * @return the number of pages on all namespaces of the wiki */		public long getPageCount { return pages; }

/**		 * Returns the number of pages on all content namespaces of the wiki for * which this <tt>Statistics</tt> was created. * 		 * @return the number of pages on all content namespaces of the wiki * @see Namespace#isContent */		public long getContentPageCount { return articles; }

/**		 * Returns the number of edits that have been made in total on the wiki * for which this <tt>Statistics</tt> was created. * 		 * @return the number of edits that have been made in total on the wiki */		public long getEditCount { return edits; }

/**		 * Returns the number of user accounts that have been made in total on * the wiki for which this <tt>Statistics</tt> was created. * 		 * @return the number of user accounts that have been made in total on		 *        the wiki */		public long getUserCount { return users; }

/**		 * Returns the number of user accounts that have been active in the last * 30 days on the wiki for which this <tt>Statistics</tt> was created. * 		 * @return the number of user accounts that have been active in the last *        30 days on the wiki */		public long getActiveUserCount { return activeUsers; }

/**		 * Returns the number of images that have been uploaded on the wiki for * which this <tt>Statistics</tt> was created. * 		 * @return the number of images that have been uploaded on the wiki */		public long getImageCount { return images; }

/**		 * Returns the number of user accounts that are administrators on the * wiki for which this <tt>Statistics</tt> was created. * 		 * @return the number of user accounts that are administrators on the *        wiki */		public long getAdministratorCount { return admins; }

/**		 * Returns the number of jobs that are pending on the wiki for which * this <tt>Statistics</tt> was created. * 		 * @return the number of jobs that are pending on the wiki */		public long getJobQueueLength { return jobs; }

/**		 * Returns the number of views for all pages on the wiki for which this * <tt>Statistics</tt> was created. * 		 * Not all wikis show this information. The return value is -1 if the * number of views is unknown. * 		 * @return the number of views for all pages on the wiki */		public long getViewCount { return views; }

@Override public String toString { return String.format("Statistics[pages: %d, articles: %d, edits: %d, images: %d, users: %d (%d active, %d administrators), job queue: %d%s]", pages, articles, edits, images, users, activeUsers, admins, jobs, views != -1 ? ", " + views + " total views" : ", unknown total views"); }	}

public static class InterwikiPrefixes { private final Map<String, MediaWiki.InterwikiPrefix> map;

InterwikiPrefixes(final Map<String, MediaWiki.InterwikiPrefix> map) { this.map = Collections.unmodifiableMap(map); }

/**		 * Returns an <tt>InterwikiPrefix</tt> object matching the given *  drawn from this <tt>InterwikiPrefixes</tt>'s * list. The return value is  if there is no such * object. * 		 * @param prefix *           The prefix to retrieve an <tt>InterwikiPrefix</tt> for. *           This value may not end with a colon , nor *           may it contain a page name. For example, *            .		 * @return a <tt>Namespace</tt> object matching the given *         		 */		public MediaWiki.InterwikiPrefix getInterwikiPrefix(final String prefix) { return map.get(prefix); }

/**		 * Returns the <tt>InterwikiPrefix</tt> object designating the other * wiki containing the page of the given name drawn from this * <tt>InterwikiPrefixes</tt>'s list. The return value is *  if there is no such other wiki, i.e. if the link * points inside the wiki that the enclosing <tt>MediaWiki</tt> * represents. * 		 * @param fullPageName *           The page to retrieve the <tt>InterwikiPrefix</tt> of. For *           example, "Paperclip" ( : on the active		 *            wiki), "Project:Administrator requests" ( 		 *            : on the active wiki, in a namespace), *           "wikipedia:Internet" (an <tt>InterwikiPrefix</tt>		 *            describing Wikipedia: on an external wiki), *           "mw:Manual:Contents" (an <tt>InterwikiPrefix</tt>		 *            describing Mediawiki.org: on an external wiki in a		 *            namespace). * @return the <tt>InterwikiPrefix</tt> object designating the other *        wiki containing the page of the given name */		public MediaWiki.InterwikiPrefix getInterwikiPrefixForPage(final String fullPageName) { int colonIndex = fullPageName.indexOf(':'); if (colonIndex == -1) return null; return getInterwikiPrefix(fullPageName.substring(0, colonIndex)); }

/**		 * Returns the name of the given page after stripping it of its * interwiki prefix. The return value is the page name itself if what is		 * before the colon is not a valid interwiki prefix for the wiki that * the enclosing <tt>MediaWiki</tt> represents. * 		 * @param fullPageName *           The page name to strip of its interwiki prefix. For *           example, "Paperclip" (in the main namespace) becomes *           "Paperclip", "Project:Administrator requests" (in the		 *            Project namespace) becomes *           "Project:Administrator requests", "wikipedia:Internet" (on		 *            another wiki) becomes "Internet". * @return the name of the given page after stripping it of its *        namespace prefix */		public String removeInterwikiPrefix(final String fullPageName) { int colonIndex = fullPageName.indexOf(':'); if (colonIndex == -1) return fullPageName; return getInterwikiPrefix(fullPageName.substring(0, colonIndex)) != null ? fullPageName.substring(colonIndex + 1) : fullPageName; }

@Override public String toString { return map.toString; }	}

public static class InterwikiPrefix { private final String name;

private final String language;

private final String urlPattern;

private final boolean isLocal;

InterwikiPrefix(final String name, final String language, final String urlPattern, final boolean isLocal) { this.name = name; this.language = language; this.urlPattern = urlPattern; this.isLocal = isLocal; }

/**		 * Returns the name of this <tt>InterwikiPrefix</tt>. This is the prefix * itself, not the name of the target wiki. * 		 * @return the name of this <tt>InterwikiPrefix</tt> */		public String getName { return name; }

/**		 * Returns the language of the wiki targetted by this * <tt>InterwikiPrefix</t>. * 		 * @return the language of the wiki targetted by this *        <tt>InterwikiPrefix</t> */		public String getLanguage { return language; }

/**		 * Returns the URL pattern of this <tt>InterwikiPrefix</tt>, containing * a  placeholder where the article name goes. */		public String getURLPattern { return urlPattern; }

/**		 * Returns whether this <tt>InterwikiPrefix</tt> refers to a wiki on the * same project (loosely defined as being controlled by the same		 * organisation) as the wiki that the enclosing <tt>MediaWiki</tt> * represents. * 		 * @return whether this <tt>InterwikiPrefix</tt> refers to a wiki on the *        same project as the wiki that the enclosing *        <tt>MediaWiki</tt> represents */		public boolean isLocal { return isLocal; }

/**		 * Returns the full name of a page having the given base name in a wiki * and starting with this <tt>InterwikiPrefix</tt>. * 		 * @param basePageName *           Base name of the page, which may contain a namespace *           prefix. * @return the full name of a page having the given base name in a wiki *        and starting with this <tt>InterwikiPrefix</tt> */		public String getFullPageName(final String basePageName) { return name + ":" + basePageName; }

/**		 * Returns the full URL to the interwiki resource having this * <tt>InterwikiPrefix</tt> as its prefix and the given page name. In		 * the case that the page name contains an interwiki prefix of its own, * it is not resolved. To resolve it, a *  is needed. * 		 * @param pageName *           Name of the page, which may contain a namespace prefix or *           another interwiki prefix. * @return the full URL to the interwiki resource having this *        <tt>InterwikiPrefix</tt> as its prefix and the given page *        name */		public String resolveURL(final String pageName) { // If $1 is at the end... if (urlPattern.endsWith("$1")) { // Put the entire page name at the end. return urlPattern.substring(0, urlPattern.length - 2) + pageName; } else { /*				 * Not at the end. Put the first colon-separated token where $1 * is, and put the rest of the page at the end. (For Wikia's				 * c:wikiname:Page thing.) */				int colonIndex = pageName.indexOf(':'); if (colonIndex == -1) { return urlPattern.replaceAll("$1", Matcher.quoteReplacement(pageName)); } else { return urlPattern.replaceAll("$1", Matcher.quoteReplacement(pageName.substring(0, colonIndex))) + pageName.substring(colonIndex + 1); }			}		}

@Override public String toString { // InterwikiPrefix["Name" -> <URL>, local: true, language: English return String.format("InterwikiPrefix[\"%s\" -> <%s>, local: %s%s]", name, urlPattern, isLocal, language != null ? ", language prefix: " + language : ""); }	}

public class PageInformation { private final boolean missing;

private final long pageID;

private final String fullName;

private final Date lastEdit;

private final long namespaceID;

private final long lastRevisionID;

private final long views;

private final long length;

private final boolean isRedirect;

private final boolean isNew;

private final Map<String, MediaWiki.Protection> protections;

PageInformation(final boolean missing, final long pageID, final String title, final Date lastEdit, final long namespaceID, final long lastRevisionID, final long views, final long length, final boolean isRedirect, final boolean isNew, final Map<String, MediaWiki.Protection> protections) { this.missing = missing; this.pageID = pageID; this.fullName = title; this.lastEdit = lastEdit; this.namespaceID = namespaceID; this.lastRevisionID = lastRevisionID; this.views = views; this.length = length; this.isRedirect = isRedirect; this.isNew = isNew; this.protections = Collections.unmodifiableMap(protections); }

/**		 * Returns  if the page designated by this * <tt>PageInformation</tt> is missing, or  if it * exists. Most other attributes will also be missing if this is the * case; however, information about page creation protection (if		 * applicable), and the namespace and title of the page will still be * available. * 		 * @return  if the page designated by this *        <tt>PageInformation</tt> is missing;   if it		 *         exists */		public boolean isMissing { return missing; }

/**		 * Returns the ID of the page designated by this * <tt>PageInformation</tt>. The return value is -1 if the page is * missing. * 		 * @return the ID of the page designated by this *        <tt>PageInformation</tt> * @see #isMissing */		public long getPageID { return pageID; }

/**		 * Returns the title of the page designated by this * <tt>PageInformation</tt>. * 		 * @return the title of the page designated by this *        <tt>PageInformation</tt> */		public String getTitle { return fullName; }

/**		 * Returns the date and time at which the page designated by this * <tt>PageInformation</tt> was last edited. The return value is *  if the page is missing. * 		 * @return the date and time at which the page designated by this *        <tt>PageInformation</tt> was last edited * @see #isMissing */		public Date getLastEdit { return lastEdit; }

/**		 * Returns the ID of the namespace containing the page designated by * this <tt>PageInformation</tt>. * 		 * @return the ID of the page designated by this *        <tt>PageInformation</tt> */		public long getNamespaceID { return namespaceID; }

/**		 * Returns the * <tt>Namespace<tt> object designating the namespace containing the page designated by this * <tt>PageInformation</tt>. * 		 * @return the *        <tt>Namespace<tt> object designating the namespace containing the page designated by this * <tt>PageInformation</tt> * @throws IOException *            if an error occurs while getting the list of namespaces *            on the wiki represented by the enclosing *            <tt>MediaWiki</tt> */		public MediaWiki.Namespace getNamespace throws IOException { return MediaWiki.this.getNamespaces.getNamespace(namespaceID); }

/**		 * Returns the full name of the page designated by this * <tt>PageInformation</tt>. * 		 * @return the full name of the page designated by this *        <tt>PageInformation</tt> * @throws IOException *            if an error occurs while getting the list of namespaces *            on the wiki represented by the enclosing *            <tt>MediaWiki</tt> */		public String getFullPageName throws IOException { return getNamespace.getFullPageName(fullName); }

/**		 * Returns the ID of the last revision made to the page designated by * this <tt>PageInformation</tt>. The return value is -1 if the page is * missing. * 		 * @return the ID of the last revision made to the page designated by		 *        this <tt>PageInformation</tt> * @see #isMissing */		public long getLastRevisionID { return lastRevisionID; }

/**		 * Returns the number of times the page designated by this * <tt>PageInformation</tt> has been viewed. The return value is -1 if		 * the page is missing, or if the wiki represented by the enclosing * <tt>MediaWiki</tt> does not give this information. * 		 * @return the number of times the page designated by this *        <tt>PageInformation</tt> has been viewed */		public long getViewCount { return views; }

/**		 * Returns the length the page designated by this * <tt>PageInformation</tt> as of its last revision. The return value is * 0 if the page is missing. * 		 * @return the length the page designated by this *        <tt>PageInformation</tt> as of its last revision * @see #isMissing */		public long getLength { return length; }

/**		 * Returns  if the page designated by this * <tt>PageInformation</tt> exists and is a redirect to another page, * and  otherwise. * 		 * Redirect pages start with  followed by a * wikilink to the targetted page. * 		 * @return  if the page designated by this *        <tt>PageInformation</tt> exists and is a redirect to another *        page, and   otherwise */		public boolean isRedirect { return isRedirect; }

/**		 * Returns  if the page designated by this * <tt>PageInformation</tt> exists and has one revision, and *  otherwise. * 		 * @return  if the page designated by this *        <tt>PageInformation</tt> exists and has one revision, and *          otherwise */		public boolean isNew { return isNew; }

/**		 * Returns a map associating action names (such as those in		 * <tt>MediaWiki.ProtectionAction</tt>) to <tt>MediaWiki.Protection</tt> * objects describing the terms of their protection. The return value is * never, but the map may be empty. * 		 * @return a map associating action names (such as those in		 *        <tt>MediaWiki.ProtectionAction</tt>) to		 *         <tt>MediaWiki.Protection</tt> objects describing the terms of		 *         their protection */		public Map<String, MediaWiki.Protection> getProtection { return protections; }

@Override public String toString { // PageInformation["0:Main Page" (missing), redirect, new, 410 bytes // long, last revision: 5207, edited on DATE, views: 61358, // protections: {edit=...}] return String.format("PageInformation[\"%s\"%s%s%s%s%s%s%s%s, protections: %s]", fullName, missing ? " (missing)" : "", pageID != -1 ? " (ID " + pageID + ")" : "", isRedirect ? ", redirect" : "", isNew ? ", new" : "", ", " + length + " bytes long", lastRevisionID != -1 ? ", last revision: " + lastRevisionID : "", lastEdit != null ? ", edited on " + lastEdit.toString : "", views != -1 ? ", views: " + views : "", protections); }	}

public class Protection { private final String level;

private final Date expiry;

private final boolean isCascading;

private final String cascadeSource;

Protection(final String level, final Date expiry, final boolean isCascading, final String cascadeSource) { this.level = level; this.expiry = expiry; this.isCascading = isCascading; this.cascadeSource = cascadeSource; }

/**		 * Returns the level required for users to be able to perform the * protected action for which this <tt>Protection</tt> was created. The * return value is equivalent to, but not necessarily the same object * as, either * or. * 		 * @return the level required for users to be able to perform the *        protected action for which this <tt>Protection</tt> was *        created */		public String getLevel { return level; }

/**		 * Returns the date and time at which the protection for which this * <tt>Protection</tt> was created is set to expire. After this, the * protected action may be performed by everyone. The return value is *  if the protection is set never to expire. * 		 * @return the date and time at which the protection for which this *        <tt>Protection</tt> was created is set to expire */		public Date getExpiry { return expiry; }

/**		 * Returns whether the protection for which this <tt>Protection</tt> was * created is cascading, i.e. whether pages transcluded into the * protected page are also to be protected. For pages that are protected * due to cascade protection on another page, this method * returns  and   returns * the source of the protection. * 		 * @return whether the protection for which this <tt>Protection</tt> was *        created is cascading */		public boolean isCascading { return isCascading; }

/**		 * Returns the full name of the page that caused the action for which * this <tt>Protection</tt> was created to be protected. The return * value is  if there is no such page. * 		 * @return whether the protection for which this <tt>Protection</tt> was *        created is cascading */		public String getCascadeSource { return cascadeSource; }

@Override public String toString { // Protection[sysop expiring DATE, cascading, from "Other page"] return String.format("Protection[%s expiring %s%s%s]", level, expiry != null ? expiry.toString : "never", isCascading ? ", cascading" : "", cascadeSource != null ? ", from \"" + cascadeSource + "\"" : ""); }	}

public class RevisionInformation { private final long revisionID;

private final long parentID;

private final Date timestamp;

private final String userName;

private final boolean userHidden;

private final long length;

private final String comment;

private final boolean commentHidden;

private final boolean isMinor;

private final boolean isAnonymous;

private String content; // TODO get this from the API

private boolean contentHidden;

private boolean contentStored;

RevisionInformation(final long revisionID, final long parentID, final Date timestamp, final String userName, final boolean userHidden, final long length, final String comment, final boolean commentHidden, final boolean isMinor, final boolean isAnonymous) { this.revisionID = revisionID; this.parentID = parentID; this.timestamp = timestamp; this.userName = userName; this.userHidden = userHidden; this.length = length; this.comment = comment; this.commentHidden = commentHidden; this.isMinor = isMinor; this.isAnonymous = isAnonymous; }

/**		 * Returns the ID of the revision for which this * <tt>RevisionInformation</tt> was created. The return value is		 * guaranteed to be greater than or equal to 0, and to be known even if * the revision is deleted. * 		 * @return the ID of the revision for which this *        <tt>RevisionInformation</tt> was created */		public long getRevisionID { return revisionID; }

/**		 * Returns the ID of the revision before the one for which this * <tt>RevisionInformation</tt> was created, from the same page. The * return value is guaranteed to be greater than or equal to 0, and to * be known even if the revision is deleted. The return value is 0 if * this revision created its page. * 		 * @return the ID of the revision before the one for which this *        <tt>RevisionInformation</tt> was created, from the same page */		public long getParentID { return parentID; }

/**		 * Returns the timestamp at which the revision for which this * <tt>RevisionInformation</tt> was created was made. The return value * is guaranteed to be non-, even if the revision is * deleted. * 		 * @return the timestamp at which the revision for which this *        <tt>RevisionInformation</tt> was created was made */		public Date getTimestamp { return timestamp; }

/**		 * Returns the name of the user who made the revision for which this * <tt>RevisionInformation</tt> was created. The return value is		 *  if the revision is deleted with the username * hidden. * 		 * @return the name of the user who made the revision for which this *        <tt>RevisionInformation</tt> was created */		public String getUserName { return userName; }

/**		 * Returns whether the name of the user who made the revision for which * this <tt>RevisionInformation</tt> was created is hidden by revision * deletion. * 		 * @return whether the name of the user who made the revision for which *        this <tt>RevisionInformation</tt> was created is hidden by		 *         revision deletion */		public boolean isUserNameHidden { return userHidden; }

/**		 * Returns the length of the page as of the revision for which this * <tt>RevisionInformation</tt> was created. * 		 * @return the length of the page as of the revision for which this *        <tt>RevisionInformation</tt> was created */		public long getLength { return length; }

/**		 * Returns the comment for the revision for which this * <tt>RevisionInformation</tt> was created. The return value is *  if the revision is deleted with the comment hidden. * 		 * @return the comment for the revision for which this *        <tt>RevisionInformation</tt> was created */		public String getComment { return comment; }

/**		 * Returns whether the comment for the revision for which this * <tt>RevisionInformation</tt> was created is hidden by revision * deletion. * 		 * @return whether the comment for the revision for which this *        <tt>RevisionInformation</tt> was created is hidden by		 *         revision deletion */		public boolean isCommentHidden { return commentHidden; }

/**		 * Returns  if any piece of information about the * revision for which this <tt>RevisionInformation</tt> was created is * deleted. * 		 * @return  if any piece of information about the *        revision for which this <tt>RevisionInformation</tt> was *        created is deleted */		public boolean isDeleted { return userHidden || commentHidden || (contentStored && contentHidden); }

/**		 * Returns whether the revision for which this * <tt>RevisionInformation</tt> was created was marked by the user who * made it as minor. * 		 * @return whether the revision for which this *        <tt>RevisionInformation</tt> was created was marked by the *        user who made it as minor */		public boolean isMinor { return isMinor; }

/**		 * Returns whether the revision for which this * <tt>RevisionInformation</tt> was created was made by an anonymous * user. The return value is undefined if the user's name is hidden by * revision deletion. * 		 * @return whether the revision for which this *        <tt>RevisionInformation</tt> was created was made by an		 *         anonymous user */		public boolean isAnonymous { return isAnonymous; }

@Override public String toString { // RevisionInformation[1337 <- 1336 @ DATE (SIZE) by 			// or USER, minor, anonymous or (COMMENT)] return String.format("RevisionInformation[%d <- %d (%d bytes) @ %s by %s%s%s %s]", revisionID, parentID, length, timestamp, userHidden ? " " : userName, isMinor ? ", minor" : "", isAnonymous ? ", anonymous" : "", commentHidden ? " " : "(" + comment + ")"); }	}

// - - - ENUMS AND ENUM-LIKE CLASSES - - -

/**	 * Useful constants for protection actions. These describe actions that are * prohibited to users who are not in certain groups. */	public static class ProtectionAction { /**		 * Protection against creation. */		public static final String CREATE = "create";

/**		 * Protection against edition. */		public static final String EDIT = "edit";

/**		 * Protection against movement. */		public static final String MOVE = "move"; }

/**	 * Useful constants for protection types. These describe the groups that * users must belong to in order to perform protected actions. */	public static class ProtectionType { /**		 * Actions require that users be autoconfirmed. * 		 * The definition of autoconfirmed varies from wiki to wiki, but * generally requires some number of edits or days since the creation of * a user account. */		public static final String AUTOCONFIRMED_USERS = "autoconfirmed";

/**		 * Actions require that users be sysops. */		public static final String SYSOPS = "sysop"; }

/**	 * Useful constants for standard namespaces. These designate namespace IDs * that are standard in many MediaWiki installations. */	public static class StandardNamespace { /**		 * The namespace identifier for the Media namespace. */		public static final long MEDIA = -2;

/**		 * The namespace identifier for the Special namespace. */		public static final long SPECIAL = -1;

/**		 * The namespace identifier for the main namespace. */		public static final long MAIN = 0;

/**		 * The namespace identifier for the Talk namespace, which contains talk * pages for the main namespace. */		public static final long TALK = 1;

/**		 * The namespace identifier for the User namespace. */		public static final long USER = 2;

/**		 * The namespace identifier for the User talk namespace. */		public static final long USER_TALK = 3;

/**		 * The namespace identifier for the Project namespace. */		public static final long PROJECT = 4;

/**		 * The namespace identifier for the Project talk namespace. */		public static final long PROJECT_TALK = 5;

/**		 * The namespace identifier for the File namespace. */		public static final long FILE = 6;

/**		 * The namespace identifier for the File talk namespace. */		public static final long FILE_TALK = 7;

/**		 * The namespace identifier for the MediaWiki namespace, which contains * system messages. */		public static final long MEDIAWIKI = 8;

/**		 * The namespace identifier for the MediaWiki talk namespace. */		public static final long MEDIAWIKI_TALK = 9;

/**		 * The namespace identifier for the Template namespace. */		public static final long TEMPLATE = 10;

/**		 * The namespace identifier for the Template talk namespace. */		public static final long TEMPLATE_TALK = 11;

/**		 * The namespace identifier for the Help namespace. */		public static final long HELP = 12;

/**		 * The namespace identifier for the Help talk namespace. */		public static final long HELP_TALK = 13;

/**		 * The namespace identifier for the Category namespace. */		public static final long CATEGORY = 14;

/**		 * The namespace identifier for the Category talk namespace. */		public static final long CATEGORY_TALK = 15; }

// - - - HELPER METHODS FOR DATA FORMAT CONVERSION - - -

private static final SimpleDateFormat timestampFormatter = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);

/**	 * Returns the specified  formatted for use as a MediaWiki * timestamp value. * 	 * @param date *           The date to format. * @return the specified  formatted for use as a MediaWiki *        timestamp value */	public static String dateToTimestamp(final Date date) { return timestampFormatter.format(date); }

// - - - HELPER METHODS FOR CONNECTIONS - - -

/**	 * Returns an instance of <tt>InputStream</tt> that reads the wiki's reply * to a GET request. * 	 * @param url *           The URL to get. * @return an instance of <tt>InputStream</tt> that reads the wiki's reply *        to the GET request * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or reading HTTP headers */	protected InputStream get(String url) throws IOException { HttpURLConnection http = (HttpURLConnection) new URL(url).openConnection; initConnection(http); initGet(http); initCookies(http); http.connect;

if (http.getResponseCode != 200) throw new MediaWiki.HttpStatusException(http.getResponseCode);

updateCookies(http);

String encoding = http.getHeaderField("Content-Encoding"); return encoding != null && encoding.equals("gzip") ? new GZIPInputStream(http.getInputStream) : http.getInputStream; }

/**	 * Returns an instance of <tt>InputStream</tt> that reads the wiki's reply * to a POST request. * 	 * @param url *           The URL to POST to. This URL may have some GET parameters. * @param params *           The parameters and their values to use for the POST data. Keys *           and values in this map are not URL-encoded. * @return an instance of <tt>InputStream</tt> that reads the wiki's reply *        to the POST request * @throws IOException *            if <tt>IOException</tt> is thrown while connecting to the *            wiki or reading HTTP headers */	protected InputStream post(String url, Map<String, String> params) throws IOException { HttpURLConnection http = (HttpURLConnection) new URL(url).openConnection; initConnection(http); initPost(http); http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); initCookies(http); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(http.getOutputStream, "ISO-8859-1")); out.write(createApiPostData(params)); out.flush; out.close; http.connect;

if (http.getResponseCode != 200) throw new MediaWiki.HttpStatusException(http.getResponseCode);

updateCookies(http);

String encoding = http.getHeaderField("Content-Encoding"); return encoding != null && encoding.equals("gzip") ? new GZIPInputStream(http.getInputStream) : http.getInputStream; }

/**	 * Initializes an HTTP connection as desired by the <tt>MediaWiki</tt> * implementation. Subclasses can override this method to specify a proxy to * use, a different read timeout or user agent string, or other settings. * 	 * @param http *           The HTTP connection to initialize. */	protected void initConnection(HttpURLConnection http) { http.setAllowUserInteraction(false); http.setConnectTimeout(15000); http.setReadTimeout(60000); http.setRequestProperty("User-Agent", "MediaWiki.java/" + VERSION); if (isUsingCompression) http.setRequestProperty("Accept-Encoding", "gzip; q=1.0"); }

/**	 * Initializes an HTTP connection for a GET request as desired by the * <tt>MediaWiki</tt> implementation. Subclasses can assume that *  was called before this method. * 	 * @param http *           The HTTP connection to initialize. */	protected void initGet(HttpURLConnection http) {}

/**	 * Initializes an HTTP connection for a POST request as desired by the * <tt>MediaWiki</tt> implementation. Subclasses can assume that *  was called before this method. * 	 * @param http *           The HTTP connection to initialize. */	protected void initPost(HttpURLConnection http) { http.setDoOutput(true); }

/**	 * Adds cookies that have been preserved by this <tt>MediaWiki</tt> to the * <tt>Cookie</tt> header of the specified HTTP connection. Subclasses can * assume that  was called before this method. * 	 * @param http *           The HTTP connection to initialize. */	protected void initCookies(HttpURLConnection http) { StringBuilder cookieHeader = new StringBuilder; preferenceLock.readLock.lock; try { for (Map.Entry<String, String> cookie : cookies.entrySet) { cookieHeader.append(cookie.getKey); cookieHeader.append("="); cookieHeader.append(cookie.getValue); cookieHeader.append("; "); }		} finally { preferenceLock.readLock.unlock; }		if (cookieHeader.length >= 2) cookieHeader.delete(cookieHeader.length - 2, cookieHeader.length); http.setRequestProperty("Cookie", cookieHeader.toString); }

private static final Pattern rfc1123DateRegex = Pattern.compile("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-9]{2}-(?:Jan|Feb|Ma[ry]|Apr|Ju[nl]|Aug|Sep|Oct|Nov|Dec)-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}) GMT$");

private static final SimpleDateFormat rfc1123DateParser = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss", Locale.US);

static { rfc1123DateParser.setTimeZone(TimeZone.getTimeZone("GMT")); }

/**	 * Preserves cookies that have been received by this <tt>MediaWiki</tt> in a	 * <tt>Set-Cookie</tt> header. * 	 * @param http *           The HTTP connection to preserve cookies from. */	protected void updateCookies(HttpURLConnection http) { String headerName; preferenceLock.writeLock.lock; try { for (int i = 1; (headerName = http.getHeaderFieldKey(i)) != null; i++) if (headerName.equalsIgnoreCase("Set-Cookie")) { String cookie = http.getHeaderField(i);

String cookieCrumb = cookie.substring(0, cookie.indexOf(';')); String name = cookieCrumb.substring(0, cookieCrumb.indexOf('=')).trim; String value = cookieCrumb.substring(cookieCrumb.indexOf('=') + 1, cookieCrumb.length).trim; cookies.put(name, value);

/* If the cookie's Expires date in the past, remove it. */					String[] cookieAttributes = cookie.substring(cookie.indexOf(';') + 1).split(";"); for (String cookieAttribute : cookieAttributes) { if (cookieAttribute.indexOf('=') == -1) continue; String attrName = cookieAttribute.substring(0, cookieAttribute.indexOf('=')).trim; String attrValue = cookieAttribute.substring(cookieAttribute.indexOf('=') + 1, cookieAttribute.length).trim; if (attrName.equalsIgnoreCase("Expires")) { Matcher m = rfc1123DateRegex.matcher(attrValue); if (m.find) { try { Date expires = rfc1123DateParser.parse(m.group(1)); if (expires.compareTo(new Date) < 0) cookies.remove(name); } catch (ParseException e) { // don't care; just keep the cookie }							}						}					}				}		} finally { preferenceLock.writeLock.unlock; }	}

protected String createApiGetUrl(Map<String, String> params) { final StringBuilder result = new StringBuilder("http://").append(host).append('/').append(scriptPath); if (scriptPath.length != 0) result.append('/'); result.append("api.php"); boolean first = true; if (params != null) for (Map.Entry<String, String> param : params.entrySet) { try { result.append(first ? '?' : '&').append(URLEncoder.encode(param.getKey, "UTF-8")).append('=').append(URLEncoder.encode(param.getValue, "UTF-8")); first = false; } catch (UnsupportedEncodingException shouldNeverHappen) { throw new InternalError("UTF-8 is not supported by this Java VM"); }			}		return result.toString; }

protected String createApiPostData(Map<String, String> params) { final StringBuilder result = new StringBuilder; boolean first = true; if (params != null) for (Map.Entry<String, String> param : params.entrySet) { try { if (!first) result.append('&'); result.append(URLEncoder.encode(param.getKey, "UTF-8")).append('=').append(URLEncoder.encode(param.getValue, "UTF-8")); first = false; } catch (UnsupportedEncodingException shouldNeverHappen) { throw new InternalError("UTF-8 is not supported by this Java VM"); }			}		return result.toString; }

// - - - CUSTOM EXCEPTIONS - - -

/**	 * Type of exception thrown when an HTTP request that is expected to return * the status code 200 (OK) returns anything in the 400 or 500 ranges. */	public static class HttpStatusException extends IOException { private static final long serialVersionUID = 1L;

private final int statusCode;

public HttpStatusException(int statusCode) { this.statusCode = statusCode; }

@Override public String getMessage { return Integer.toString(statusCode); }

@Override public String getLocalizedMessage { return getMessage; }

public int getStatusCode { return statusCode; }	}

public static class MediaWikiException extends Exception { private static final long serialVersionUID = 1L;

public MediaWikiException { super; }

public MediaWikiException(String message, Throwable cause) { super(message, cause); }

public MediaWikiException(String message) { super(message); }

public MediaWikiException(Throwable cause) { super(cause); }

}

/**	 * Type of exception thrown when a username or password used to log into a	 * MediaWiki wiki is incorrect. */	public static class LoginFailureException extends MediaWiki.MediaWikiException { private static final long serialVersionUID = 1L;

public LoginFailureException {}

public LoginFailureException(String message) { super(message); }	}

/**	 * Type of exception thrown when a login on a MediaWiki wiki is refused due * to excessive login attempts for a user. */	public static class LoginDelayException extends MediaWiki.MediaWikiException { private static final long serialVersionUID = 1L;

private final int waitTime;

public LoginDelayException(int waitTime) { this.waitTime = waitTime; }

@Override public String getMessage { return Integer.toString(waitTime); }

@Override public String getLocalizedMessage { return getMessage; }

public int getWaitTime { return waitTime; }	}

/**	 * Type of exception thrown when anything that is attempted on a MediaWiki * wiki returns that the user or his/her IP address is blocked. */	public static class BlockException extends MediaWiki.MediaWikiException { private static final long serialVersionUID = 1L;

public BlockException {}

public BlockException(String message) { super(message); }	}

/**	 * Type of exception thrown when an unknown error occurs while attempting * anything on a MediaWiki wiki.. */	public static class UnknownError extends MediaWiki.MediaWikiException { private static final long serialVersionUID = 1L;

public UnknownError {}

public UnknownError(String message) { super(message); }	}

/**	 * Type of unchecked exception thrown when an iterator returned by a	 * <tt>MediaWiki</tt> method encounters an exception. */	public static class IterationException extends RuntimeException { private static final long serialVersionUID = 1L;

public IterationException {}

public IterationException(String message, Throwable cause) { super(message, cause); }

public IterationException(String message) { super(message); }

public IterationException(Throwable cause) { super(cause); }	} }