newPages(Wiki.RequestHelper helper) throws IOException
{
- return recentChanges(amount, rcoptions, true, ns);
+ return recentChanges(helper, "new");
}
/**
- * Fetches the amount most recent changes in the main namespace.
- * WARNING: The
- * recentchanges table stores new pages for a finite period of
- * time; it is not possible to retrieve pages created before then.
+ * Fetches recent edits to this wiki. See {@link
+ * #recentChanges(Wiki.RequestHelper, String)} for full documentation.
* Equivalent to [[Special:Recentchanges]].
- *
- * Note: Log entries in recent changes have a revid of 0!
*
- * @param amount the number of entries to return (overrides global query
- * limits)
+ * @param helper a {@link Wiki.RequestHelper} (optional, use null to not
+ * provide any optional parameters
* @return the recent changes that satisfy these criteria
* @throws IOException if a network error occurs
* @since 0.23
*/
- public Revision[] recentChanges(int amount) throws IOException
+ public List recentChanges(Wiki.RequestHelper helper) throws IOException
{
- return recentChanges(amount, 0, false, MAIN_NAMESPACE);
+ return recentChanges(helper, null);
}
/**
- * Fetches the amount most recent changes in the specified
- * namespace. WARNING: The recent changes table only stores new pages for
- * about a month. It is not possible to retrieve changes before then.
- * Equivalent to [[Special:Recentchanges]].
+ * Fetches recent changes to this wiki. WARNING: The recentchanges
+ * table stores edits for a
+ * finite period of time; it is not possible to retrieve pages created
+ * before then. Equivalent to [[Special:Recentchanges]].
+ *
*
- * Note: Log entries in recent changes have a revid of 0!
+ * Accepted parameters from helper are:
+ *
+ * - {@link Wiki.RequestHelper#withinDateRange(OffsetDateTime,
+ * OffsetDateTime) date range}
+ *
- {@link Wiki.RequestHelper#byUser(String) user}
+ *
- {@link Wiki.RequestHelper#notByUser(String) not by user}
+ *
- {@link Wiki.RequestHelper#reverse(boolean) reverse}
+ *
- {@link Wiki.RequestHelper#inNamespaces(int...) namespaces}
+ *
- {@link Wiki.RequestHelper#taggedWith(String) tag}
+ *
- {@link Wiki.RequestHelper#filterBy(Map) filter by}: "minor", "bot",
+ * "anon", "redirect", "patrolled"
+ *
- {@link Wiki.RequestHelper#limitedTo(int) local query limit}
+ *
*
- * @param amount the number of entries to return (overrides global query
- * limits)
- * @param ns a list of namespaces to filter by, empty = all namespaces.
- * @return the recent changes that satisfy these criteria
- * @throws IOException if a network error occurs
- * @since 0.23
- */
- public Revision[] recentChanges(int amount, int[] ns) throws IOException
- {
- return recentChanges(amount, 0, false, ns);
- }
-
- /**
- * Fetches the amount most recent changes in the specified
- * namespace subject to the specified constraints. WARNING: The recent
- * changes table only stores new pages for about a month. It is not
- * possible to retrieve changes before then. Equivalent to
- * [[Special:Recentchanges]].
*
- * Note: Log entries in recent changes have a revid of 0!
+ * If {@code rctype} is not {@code "edit"} or {@code "new"} then the results
+ * consist of pseudo-revisions whose data does not correspond to an actual
+ * on-wiki state. For example:
*
- * @param amount the number of entries to return (overrides global query
- * limits)
- * @param ns a list of namespaces to filter by, empty = all namespaces.
- * @param rcoptions a bitmask of HIDE_ANON etc that dictate which pages
- * we return.
+ *
+ * - {@code rctype == "log"} yields {@code id == 0} and {@code title} is
+ * the log entry target
+ *
- {@code rctype == "external"} yields {@code id} as the most recent
+ * edit to {@code title}, {@code previous_id == id}, {@code user} is
+ * the external user making the change, {@code sizediff == 0} and
+ * {@code comment} describes the external change
+ *
- {@code rctype =="categorize"} yields {@code title} as the category
+ * added or removed and {@code comment} specifies the page added or
+ * removed to that category
+ *
+ *
+ * @param helper a {@link Wiki.RequestHelper} (optional, use null to not
+ * provide any of the optional parameters described above
+ * @param rctype null, "edit" (edits only) or "new" (new pages); your
+ * mileage may vary for other types (log, external, categorize)
* @return the recent changes that satisfy these criteria
* @throws IOException if a network error occurs
- * @since 0.23
+ * @since 0.35
*/
- public Revision[] recentChanges(int amount, int rcoptions, int... ns) throws IOException
+ protected List recentChanges(Wiki.RequestHelper helper, String rctype) throws IOException
{
- return recentChanges(amount, rcoptions, false, ns);
- }
+ int limit = -1;
+ Map getparams = new HashMap<>();
+ getparams.put("list", "recentchanges");
+ getparams.put("rcprop", "title|ids|user|timestamp|flags|comment|parsedcomment|sizes|sha1|tags");
+ if (helper != null)
+ {
+ helper.setRequestType("rc");
+ getparams.putAll(helper.addNamespaceParameter());
+ getparams.putAll(helper.addUserParameter());
+ getparams.putAll(helper.addExcludeUserParameter());
+ getparams.putAll(helper.addDateRangeParameters());
+ getparams.putAll(helper.addTagParameter());
+ getparams.putAll(helper.addReverseParameter());
+ getparams.putAll(helper.addShowParameter());
+ limit = helper.limit();
+ }
- /**
- * Fetches the amount most recent changes in the specified
- * namespace subject to the specified constraints. WARNING: The recent
- * changes table only stores new pages for about a month. It is not
- * possible to retrieve changes before then. Equivalent to
- * [[Special:Recentchanges]].
- *
- * Note: Log entries in recent changes have a revid of 0!
- *
- * @param amount the number of entries to return (overrides global
- * query limits)
- * @param ns a list of namespaces to filter by, empty = all namespaces.
- * @param rcoptions a bitmask of HIDE_ANON etc that dictate which pages
- * we return.
- * @param newpages show new pages only
- * @return the recent changes that satisfy these criteria
- * @throws IOException if a network error occurs
- * @since 0.23
- */
- protected Revision[] recentChanges(int amount, int rcoptions, boolean newpages, int... ns) throws IOException
- {
- StringBuilder url = new StringBuilder(query);
- url.append("list=recentchanges&rcprop=title%7Cids%7Cuser%7Ctimestamp%7Cflags%7Ccomment%7Csizes%7Csha1");
- constructNamespaceString(url, "rc", ns);
- if (newpages)
- url.append("&rctype=new");
- // rc options
- if (rcoptions > 0)
- {
- url.append("&rcshow=");
- if ((rcoptions & HIDE_ANON) == HIDE_ANON)
- url.append("!anon%7C");
- if ((rcoptions & HIDE_SELF) == HIDE_SELF)
- url.append("!self%7C");
- if ((rcoptions & HIDE_MINOR) == HIDE_MINOR)
- url.append("!minor%7C");
- if ((rcoptions & HIDE_PATROLLED) == HIDE_PATROLLED)
- url.append("!patrolled%7C");
- if ((rcoptions & HIDE_BOT) == HIDE_BOT)
- url.append("!bot%7C");
- // chop off last |
- url.delete(url.length() - 3, url.length());
- }
-
- int originallimit = getQueryLimit();
- setQueryLimit(amount);
- List revisions = queryAPIResult("rc", url, newpages ? "newPages" : "recentChanges",
+ if (rctype != null)
+ getparams.put("rctype", rctype);
+
+ List revisions = makeListQuery("rc", getparams, null, "recentChanges", limit,
(line, results) ->
{
// xml form
for (int i = line.indexOf(" 0; i = line.indexOf("", i);
+ int j = line.indexOf("", i);
results.add(parseRevision(line.substring(i, j), ""));
}
});
- setQueryLimit(originallimit);
- int temp = revisions.size();
- log(Level.INFO, "recentChanges", "Successfully retrieved recent changes (" + temp + " revisions)");
- return revisions.toArray(new Revision[temp]);
+ log(Level.INFO, "recentChanges", "Successfully retrieved recent changes (" + revisions.size() + " revisions)");
+ return revisions;
}
/**
* Fetches all pages that use interwiki links to the specified wiki and the
- * page on that wiki that is linked to. For example,
- * getInterWikiBacklinks("testwiki")
may return:
+ * page on that wiki that is linked to. For example, {@code
+ * getInterWikiBacklinks("testwiki")} may return:
*
* {
* { "Spam", "testwiki:Blah" },
@@ -5844,7 +6449,7 @@ protected Revision[] recentChanges(int amount, int rcoptions, boolean newpages,
* @throws IOException if a network error occurs
* @since 0.23
*/
- public String[][] getInterWikiBacklinks(String prefix) throws IOException
+ public List getInterWikiBacklinks(String prefix) throws IOException
{
return getInterWikiBacklinks(prefix, "|");
}
@@ -5858,8 +6463,8 @@ public String[][] getInterWikiBacklinks(String prefix) throws IOException
*
*
* Example: If [[Test]] and [[Spam]] both contain the interwiki link
- * [[testwiki:Blah]] then getInterWikiBacklinks("testwiki", "Blah");
- *
will return (sorted by title)
+ * [[testwiki:Blah]] then {@code getInterWikiBacklinks("testwiki", "Blah");}
+ * will return (sorted by title)
*
* {
* { "Spam", "testwiki:Blah" },
@@ -5882,23 +6487,20 @@ public String[][] getInterWikiBacklinks(String prefix) throws IOException
* prefix (the MediaWiki API doesn't like this)
* @since 0.23
*/
- public String[][] getInterWikiBacklinks(String prefix, String title) throws IOException
+ public List getInterWikiBacklinks(String prefix, String title) throws IOException
{
// must specify a prefix
if (title.equals("|") && prefix.isEmpty())
throw new IllegalArgumentException("Interwiki backlinks: title specified without prefix!");
- StringBuilder url = new StringBuilder(query);
- url.append("list=iwbacklinks&iwblprefix=");
- url.append(prefix);
+ Map getparams = new HashMap<>();
+ getparams.put("list", "iwbacklinks");
+ getparams.put("iwblprefix", prefix);
if (!title.equals("|"))
- {
- url.append("&iwbltitle=");
- url.append(title);
- }
- url.append("&iwblprop=iwtitle%7Ciwprefix");
+ getparams.put("iwbltitle", normalize(title));
+ getparams.put("iwblprop", "iwtitle|iwprefix");
- List links = queryAPIResult("iwbl", url, "getInterWikiBacklinks", (line, results) ->
+ List links = makeListQuery("iwbl", getparams, null, "getInterWikiBacklinks", -1, (line, results) ->
{
// xml form:
for (int x = line.indexOf(" 0; x = line.indexOf("
{
- private String username;
- private String[] rights = null; // cache
- private String[] groups = null; // cache
+ private final String username;
+ private final OffsetDateTime registration;
+ // user privileges (volatile, changes rarely)
+ private List rights;
+ private List groups;
+ private boolean blocked;
+ // user preferences (volatile, changes rarely)
+ private Gender gender;
+ private boolean emailable;
+ // volatile, changes often
+ private int editcount;
/**
* Creates a new user object. Does not create a new user on the
@@ -5933,11 +6543,27 @@ public class User implements Cloneable, Serializable
* be called for anons.
*
* @param username the username of the user
+ * @param registration when the user was registered
+ * @param rights the rights this user has
+ * @param groups the groups this user belongs to
+ * @param gender the self-declared {@link Wiki.Gender Gender} of this user.
+ * @param emailable whether the user can be emailed through [[Special:Emailuser]]
+ * @param blocked whether this user is blocked
+ * @param editcount the internal edit count of this user
* @since 0.05
*/
- protected User(String username)
+ protected User(String username, OffsetDateTime registration, List rights, List groups,
+ Gender gender, boolean emailable, boolean blocked,int editcount)
{
- this.username = username;
+ this.username = Objects.requireNonNull(username);
+ // can be null per https://phabricator.wikimedia.org/T24097
+ this.registration = registration;
+ this.rights = Objects.requireNonNull(rights);
+ this.groups = Objects.requireNonNull(groups);
+ this.gender = gender;
+ this.emailable = emailable;
+ this.blocked = blocked;
+ this.editcount = editcount;
}
/**
@@ -5945,111 +6571,131 @@ protected User(String username)
* @return this user's username
* @since 0.08
*/
- public String getUsername()
+ public final String getUsername()
{
return username;
}
/**
- * Gets various properties of this user. Returns:
- *
- * - editcount: (int) {@link #countEdits()} the user's edit
- * count
- *
- groups: (String[]) the groups the user is in (see
- * [[Special:Listgrouprights]])
- *
- rights: (String[]) the stuff the user can do
- *
- emailable: (Boolean) whether the user can be emailed
- * through [[Special:Emailuser]] or emailUser()
- *
- blocked: (Boolean) whether the user is blocked
- *
- gender: (Wiki.Gender) the user's gender
- *
- created: (Calendar) when the user account was created
- *
- *
+ * Gets the date/time at which this user account was created. May be
+ * {@code null} per
+ * https://phabricator.wikimedia.org/T24097.
* @return (see above)
- * @throws IOException if a network error occurs
- * @since 0.24
+ * @since 0.35
*/
- public Map getUserInfo() throws IOException
+ public final OffsetDateTime getRegistrationDate()
{
- return Wiki.this.getUserInfo(new String[] { username })[0];
+ return registration;
}
/**
- * Returns true if the user is allowed to perform the specified action.
- * Uses the rights cache. Read [[Special:Listgrouprights]] before using
- * this!
+ * Returns {@code true} if the user is allowed to perform the specified
+ * action(s). Read [[Special:Listgrouprights]] before using this!
* @param right a specific action
- * @return whether the user is allowed to execute it
+ * @param morerights additional actions to check
+ * @return whether the user is allowed to execute them
* @since 0.24
- * @throws IOException if a network error occurs
*/
- public boolean isAllowedTo(String right) throws IOException
- {
- // We can safely assume the user is allowed to { read, edit, create,
- // writeapi }.
- if (rights == null)
- rights = (String[])getUserInfo().get("rights");
- for (String r : rights)
- if (r.equals(right))
- return true;
- return false;
+ public boolean isAllowedTo(String right, String... morerights)
+ {
+ List temp = new ArrayList<>();
+ temp.add(right);
+ temp.addAll(List.of(morerights));
+ return rights.containsAll(temp);
}
/**
- * Returns true if the user is a member of the specified group. Uses
- * the groups cache.
+ * Returns {@code true} if the user is a member of the specified group.
* @param group a specific group
* @return whether the user is in it
* @since 0.24
- * @throws IOException if a network error occurs
*/
- public boolean isA(String group) throws IOException
+ public boolean isA(String group)
{
- if (groups == null)
- groups = (String[])getUserInfo().get("groups");
- for (String g : groups)
- if (g.equals(group))
- return true;
- return false;
+ return groups.contains(group);
}
/**
- * Returns a log of the times when the user has been blocked.
- * @return records of the occasions when this user has been blocked
- * @throws IOException if something goes wrong
- * @since 0.08
+ * Returns the groups the user is a member of. See [[Special:Listgrouprights]].
+ * Changes in this list do not propagate to this object or the wiki.
+ * @return (see above)
+ * @since 0.35
+ */
+ public List getGroups()
+ {
+ return new ArrayList<>(groups);
+ }
+
+ /**
+ * Returns the specific permissions this user has. See [[Special:Listgrouprights]].
+ * Changes in this list do not propagate to the object or the wiki.
+ * @return (see above)
+ * @since 0.35
+ */
+ public List getRights()
+ {
+ return new ArrayList<>(rights);
+ }
+
+ /**
+ * Returns whether this user can be emailed through [[Special:Emailuser]].
+ * @return (see above)
+ * @see #emailUser(Wiki.User, String, String, boolean)
+ * @since 0.35
+ */
+ public boolean canBeEmailed()
+ {
+ return emailable;
+ }
+
+ /**
+ * Returns the self-disclosed {@linkplain Wiki.Gender gender} of this
+ * user.
+ * @return (see above)
+ * @see Wiki.Gender
+ * @since 0.35
*/
- public LogEntry[] blockLog() throws IOException
+ public Gender getGender()
{
- return Wiki.this.getLogEntries(Wiki.BLOCK_LOG, null, null, "User:" + username);
+ return gender;
}
/**
- * Determines whether this user is blocked by looking it up on the IP
- * block list.
+ * Determines whether this user is blocked at the time of construction.
+ * If you want a live check, look up the user on the {@linkplain
+ * #getBlockList list of blocks}.
* @return whether this user is blocked
- * @throws IOException if we cannot retrieve the IP block list
* @since 0.12
*/
- public boolean isBlocked() throws IOException
+ public boolean isBlocked()
{
- // @revised 0.18 now check for errors after each edit, including blocks
- return getIPBlockList(username, null, null).length != 0;
+ return blocked;
}
/**
- * Fetches the internal edit count for this user, which includes all
- * live edits and deleted edits after (I think) January 2007. If you
- * want to count live edits only, use the slower
- * int count = {@link User#contribs(int...) user.contribs()}.length;
.
+ * Fetches the internal edit count for this user at the time of
+ * construction, which includes all live edits and deleted edits after
+ * (I think) January 2007. If you want to count live edits only,
+ * compute the size of {@link User#contribs(int...) User.contribs()}.
*
* @return the user's edit count
- * @throws IOException if a network error occurs
* @since 0.16
*/
- public int countEdits() throws IOException
+ public int countEdits()
+ {
+ return editcount;
+ }
+
+ /**
+ * Returns a log of the times when the user has been blocked.
+ * @return records of the occasions when this user has been blocked
+ * @throws IOException if something goes wrong
+ * @since 0.08
+ */
+ public List blockLog() throws IOException
{
- return (Integer)getUserInfo().get("editcount");
+ Wiki.RequestHelper rh = new RequestHelper().byTitle("User:" + username);
+ return Wiki.this.getLogEntries(Wiki.BLOCK_LOG, null, rh);
}
/**
@@ -6059,243 +6705,430 @@ public int countEdits() throws IOException
* @throws IOException if a network error occurs
* @since 0.17
*/
- public Revision[] contribs(int... ns) throws IOException
+ public List contribs(int... ns) throws IOException
{
- return Wiki.this.contribs(username, ns);
+ Wiki.RequestHelper rh = new RequestHelper().inNamespaces(ns);
+ return Wiki.this.contribs(username, rh);
}
-
+
/**
* Returns the list of logged actions performed by this user.
* @param logtype what log to get ({@link Wiki#DELETION_LOG} etc.)
- * @param action what action to get (e.g. delete, undelete), use
+ * @param action what action to get (e.g. delete, undelete), use
* "" to not specify one
* @return (see above)
* @throws IOException if a network error occurs
* @since 0.33
*/
- public LogEntry[] getLogEntries(String logtype, String action) throws IOException
+ public List getLogEntries(String logtype, String action) throws IOException
{
- return Wiki.this.getLogEntries(logtype, action, username, null);
+ Wiki.RequestHelper rh = new RequestHelper().byUser(username);
+ return Wiki.this.getLogEntries(logtype, action, rh);
}
/**
- * Copies this user object.
- * @return the copy
- * @throws CloneNotSupportedException if the clone fails
- * @since 0.08
+ * Tests whether this user is equal to another one.
+ * @param x another object
+ * @return whether the usernames of the users are equal
*/
@Override
- public User clone() throws CloneNotSupportedException
+ public boolean equals(Object x)
{
- try
- {
- return (User)super.clone();
- }
- catch (CloneNotSupportedException e)
- {
- return null;
- }
+ if (!(x instanceof User))
+ return false;
+ User other = (User)x;
+ return Objects.equals(username, other.username)
+ && Objects.equals(registration, other.registration);
}
/**
- * Tests whether this user is equal to another one.
- * @param x another object
- * @return whether the usernames of the users are equal
- * @since 0.08
+ * Returns a hashcode of this user based on the username and
+ * registration date.
+ * @return see above
*/
@Override
- public boolean equals(Object x)
+ public int hashCode()
{
- return x instanceof User && username.equals(((User)x).username);
+ return username.hashCode() * 127 + registration.hashCode();
}
/**
- * Returns a string representation of this user.
- * @return see above
- * @since 0.17
+ * Enables sorting of users by their username.
+ * @param other some other user
+ * @return less than zero if this user is alphabetically before the
+ * other, 0 if they are the same and 1 if alphabetically after
*/
@Override
- public String toString()
+ public int compareTo(User other)
{
- StringBuilder temp = new StringBuilder("User[username=");
- temp.append(username);
- temp.append("groups=");
- temp.append(groups != null ? Arrays.toString(groups) : "unset");
- temp.append("]");
- return temp.toString();
+ return username.compareTo(other.username);
}
/**
- * Returns a hashcode of this user.
+ * Returns a string representation of this user.
* @return see above
- * @since 0.19
*/
@Override
- public int hashCode()
+ public String toString()
{
- return username.hashCode() * 2 + 1;
+ return getClass().getName()
+ + "[username=" + username
+ + ",registration=" + (registration != null ? registration.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) : "unset")
+ + ",groups=" + Arrays.toString(groups.toArray()) + "]";
}
}
/**
- * A wrapper class for an entry in a wiki log, which represents an action
- * performed on the wiki.
- *
- * @see #getLogEntries
- * @since 0.08
+ * A data super class for an event happening on a wiki such as a {@link
+ * Wiki.Revision} or a {@link Wiki.LogEntry}.
+ * @since 0.35
*/
- public class LogEntry implements Comparable
+ public abstract class Event implements Comparable
{
- // internal data storage
- private long logid = -1;
- private String type;
- private String action;
- private final String reason;
- private User user;
- private String target;
+ private final long id;
private final OffsetDateTime timestamp;
- private Object details;
- private boolean reasonDeleted = false, userDeleted = false,
- targetDeleted = false;
+ private final String user;
+ private final String title;
+ private final String comment;
+ private final String parsedcomment;
+ private List tags;
+ private boolean commentDeleted = false, userDeleted = false,
+ contentDeleted = false;
/**
- * Creates a new log entry. WARNING: does not perform the action
- * implied. Use Wiki.class methods to achieve this.
- *
- * @param type the type of log entry, one of {@link #USER_CREATION_LOG},
- * {@link #DELETION_LOG}, {@link #BLOCK_LOG}, etc.
- * @param action the type of action that was performed e.g. "delete",
- * "unblock", "overwrite", etc.
- * @param reason why the action was performed
- * @param user the user who performed the action
- * @param target the target of the action
- * @param timestamp the local time when the action was performed.
- * We will convert this back into an OffsetDateTime
- * @param details the details of the action (e.g. the new title of
- * the page after a move was performed).
- * @since 0.08
+ * Creates a new Event record.
+ * @param id the unique ID of the event
+ * @param timestamp the timestamp at which it occurred
+ * @param user the user or IP address performing the event
+ * @param title the title of the page affected
+ * @param comment the comment left by the user when performing the
+ * event (e.g. an edit summary)
+ * @param parsedcomment comment, but parsed into HTML
*/
- protected LogEntry(String type, String action, String reason, User user,
- String target, OffsetDateTime timestamp, Object details)
+ protected Event(long id, OffsetDateTime timestamp, String user, String title, String comment, String parsedcomment)
{
- this.type = type;
- this.action = action;
- this.reason = reason;
+ this.id = id;
+ this.timestamp = Objects.requireNonNull(timestamp);
this.user = user;
- this.target = target;
- this.timestamp = timestamp;
- this.details = details;
+ this.title = title;
+ this.comment = comment;
+ // Rewrite parsedcomments to fix useless relative hyperlinks to
+ // other wiki pages
+ if (parsedcomment == null)
+ this.parsedcomment = null;
+ else
+ this.parsedcomment = parsedcomment.replace("href=\"/wiki", "href=\"" + protocol + domain + "/wiki");
}
-
+
+ /**
+ * Gets the unique ID of this event. For a {@link Wiki.Revision}, this
+ * number is referred to as the "oldid" or "revid" and should not be
+ * confused with "rcid" (which is the ID in the recentchanges table).
+ * For a {@link Wiki.LogEntry}, this value only makes sense if the
+ * record was obtained through {@link Wiki#getLogEntries(String, String,
+ * RequestHelper)} and overloads (other methods return pseudo-LogEntries).
+ * @return the ID of this revision
+ */
+ public long getID()
+ {
+ return id;
+ }
+
/**
- * Gets the ID of this log entry. Only available if retrieved by
- * {@link Wiki#getLogEntries}, otherwise returns -1.
+ * Gets the timestamp of this event.
+ * @return the timestamp of this event
+ */
+ public OffsetDateTime getTimestamp()
+ {
+ return timestamp;
+ }
+
+ /**
+ * Returns the user or anon who performed this event. You should pass
+ * this (if not an IP) to {@link #getUser} to obtain a {@link
+ * Wiki.User} object. Returns {@code null} if the user was
+ * RevisionDeleted and you lack the necessary privileges.
+ * @return the user or anon
+ */
+ public String getUser()
+ {
+ return user;
+ }
+
+ /**
+ * Sets a boolean flag that the user triggering this event has been
+ * RevisionDeleted in on-wiki records.
+ * @param deleted (see above)
+ * @see #isUserDeleted()
+ * @see #getUser()
+ */
+ protected void setUserDeleted(boolean deleted)
+ {
+ userDeleted = deleted;
+ }
+
+ /**
+ * Returns {@code true} if the user triggering this event is
+ * RevisionDeleted.
* @return (see above)
- * @since 0.33
+ * @see #getUser()
*/
- public long getLogID()
+ public boolean isUserDeleted()
{
- return logid;
+ return userDeleted;
}
/**
- * Gets the type of log that this entry is in.
- * @return one of {@link Wiki#DELETION_LOG}, {@link Wiki#BLOCK_LOG}, etc.
- * @since 0.08
+ * Returns the page affected by this event. May be {@code null} for
+ * certain types of LogEntry and/or if the LogEntry is RevisionDeleted
+ * and you don't have the ability to access it.
+ * @return (see above)
+ * @see #isContentDeleted()
*/
- public String getType()
+ public String getTitle()
{
- return type;
+ return title;
}
/**
- * Gets a string description of the action performed, for example
- * "delete", "protect", "overwrite", ... WARNING: returns null if the
- * action was RevisionDeleted.
- * @return the type of action performed
- * @since 0.08
+ * Gets the comment for this event in wikitext. If this is a {@link
+ * Wiki.Revision}, this is the edit summary. If this is a {@link
+ * Wiki.LogEntry}, this is the reason for the logged action. WARNING:
+ * returns {@code null} if the reason was RevisionDeleted and you lack
+ * the necessary privileges.
+ * @return the comment associated with the event
+ * @see #getParsedComment()
*/
- public String getAction()
+ public String getComment()
{
- return action;
+ return comment;
}
-
+
+ /**
+ * Gets the comment for this event, with limited parsing into HTML.
+ * Hyperlinks in the returned HTML are rewritten from useless relative
+ * URLs to full URLs that point to the wiki page in question. Returns
+ * {@code null} if {@linkplain #isCommentDeleted() the comment was
+ * RevisionDeleted} and you lack the necessary privileges.
+ *
+ * Warnings:
+ *
+ * - Not available through {@link #getBlockList}.
+ *
+ *
+ * @return the comment associated with the event, parsed into HTML
+ * @see #getComment()
+ */
+ public String getParsedComment()
+ {
+ return parsedcomment;
+ }
+
+ /**
+ * Sets a boolean flag that the comment associated with this event has
+ * been RevisionDeleted in on-wiki records.
+ * @param deleted (see above)
+ * @see #getComment
+ * @see #getParsedComment()
+ * @see #isCommentDeleted()
+ */
+ protected void setCommentDeleted(boolean deleted)
+ {
+ commentDeleted = deleted;
+ }
+
/**
- * Returns true if the target has been RevisionDeleted (action is hidden
- * in the GUI but retrievable by the API).
+ * Returns {@code true} if the comment is RevisionDeleted.
* @return (see above)
- * @since 0.32
+ * @see #getComment
+ * @see #getParsedComment()
*/
- public boolean isTargetDeleted()
+ public boolean isCommentDeleted()
{
- return targetDeleted;
+ return commentDeleted;
}
/**
- * Gets the reason supplied by the perfoming user when the action
- * was performed. WARNING: returns null if the reason was
- * RevisionDeleted and one does not have access to the content.
- * @return the reason the action was performed
- * @since 0.08
+ * Sets a boolean flag that the content of this event has been
+ * RevisionDeleted.
+ * @param deleted (see above)
+ * @see #isContentDeleted()
*/
- public String getReason()
+ protected void setContentDeleted(boolean deleted)
{
- return reason;
+ contentDeleted = deleted;
+ }
+
+ /**
+ * Returns {@code true} if the content of this event has been
+ * RevisionDeleted. For a {@link Wiki.LogEntry}, this refers to the
+ * page the logged action affects and the logged action performed (e.g.
+ * "unblock" or "delete").
+ * @return (see above)
+ */
+ public boolean isContentDeleted()
+ {
+ return contentDeleted;
}
/**
- * Returns true if the reason is RevisionDeleted.
+ * Returns the list of tags attached to this event. Modifying the
+ * return value does not affect this Revision object or the wiki state.
* @return (see above)
- * @since 0.32
+ * @see MediaWiki
+ * documentation
+ * @since 0.37
+ */
+ public List getTags()
+ {
+ return new ArrayList<>(tags);
+ }
+
+ /**
+ * Sets the list of tags attached to this event. Modifying the supplied
+ * list does not affect this Revision object or change on-wiki state.
+ * @param tags a list of change tags
+ * @see MediaWiki
+ * documentation
+ * @since 0.37
*/
- public boolean isReasonDeleted()
+ protected void setTags(List tags)
{
- return reasonDeleted;
+ this.tags = new ArrayList<>(tags);
}
/**
- * Gets the user object representing who performed the action.
- * WARNING: returns null if the user was RevisionDeleted and one does
- * not have access to the content.
- * @return the user who performed the action.
- * @since 0.08
+ * Returns a String representation of this Event. Subclasses only need
+ * to lop off the trailing "]" and add their own fields when overriding
+ * this method.
+ * @return (see above)
*/
- public User getUser()
+ @Override
+ public String toString()
{
- return user;
+ return getClass().getName()
+ + "[id=" + id
+ + ",timestamp=" + timestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
+ + ",user=\"" + Objects.toString(user, "[DELETED]") + "\""
+ + ",userDeleted=" + userDeleted
+ + ",title=\"" + Objects.toString(title, "[null or deleted]") + "\""
+ + ",comment=\"" + Objects.toString(comment, "[DELETED]") + "\""
+ + ",commentDeleted=" + commentDeleted
+ + ",contentDeleted=" + contentDeleted + ']';
}
-
+
/**
- * Returns true if the user who performed this LogEntry is
- * RevisionDeleted.
+ * Determines whether this Event is equal to some other object. This
+ * method checks the ID, timestamp, user, title and comment.
+ * @param other the other object to compare to
+ * @return whether this instance is equal to that object
+ */
+ @Override
+ public boolean equals(Object other)
+ {
+ if (!(other instanceof Event))
+ return false;
+ Event event = (Event)other;
+ return id == event.id
+ && Objects.equals(timestamp, event.timestamp)
+ && Objects.equals(user, event.user)
+ && Objects.equals(title, event.title)
+ && Objects.equals(comment, event.comment);
+ }
+
+ /**
+ * Returns a hash code for this object based on the ID, timestamp,
+ * user, title and comment.
* @return (see above)
- * @since 0.32
*/
- public boolean isUserDeleted()
+ @Override
+ public int hashCode()
{
- return userDeleted;
+ int hc = Long.hashCode(id);
+ hc = 127 * hc + timestamp.hashCode();
+ hc = 127 * hc + Objects.hashCode(user);
+ hc = 127 * hc + Objects.hashCode(title);
+ hc = 127 * hc + Objects.hashCode(comment);
+ return hc;
+ }
+
+ /**
+ * Compares this event to another one based on the recentness of their
+ * timestamps (more recent = positive return value), then
+ * alphabetically by user.
+ * @param other the event to compare to
+ * @return the comparator value, negative if less, positive if greater
+ */
+ @Override
+ public int compareTo(Wiki.Event other)
+ {
+ int result = timestamp.compareTo(other.timestamp);
+ if (result == 0 && user != null)
+ result = user.compareTo(other.user);
+ return result;
+ }
+ }
+
+ /**
+ * A wrapper class for an entry in a wiki log, which represents an action
+ * performed on the wiki.
+ *
+ * @see #getLogEntries
+ * @since 0.08
+ */
+ public class LogEntry extends Event
+ {
+ private final String type;
+ private String action;
+ private Map details;
+
+ /**
+ * Creates a new log entry. WARNING: does not perform the action
+ * implied. Use Wiki.class methods to achieve this.
+ *
+ * @param id the unique of this log entry
+ * @param timestamp the local time when the action was performed.
+ * @param user the user who performed the action
+ * @param comment why the action was performed
+ * @param parsedcomment like comment, but parsed into HTML
+ * @param type the type of log entry, one of {@link #USER_CREATION_LOG},
+ * {@link #DELETION_LOG}, {@link #BLOCK_LOG}, etc.
+ * @param action the type of action that was performed e.g. "delete",
+ * "unblock", "overwrite", etc.
+ * @param target the target of the action
+ * @param details the details of the action (e.g. the new title of
+ * the page after a move was performed).
+ * @since 0.08
+ */
+ protected LogEntry(long id, OffsetDateTime timestamp, String user, String comment,
+ String parsedcomment, String type, String action, String target, Map details)
+ {
+ super(id, timestamp, user, target, comment, parsedcomment);
+ this.type = Objects.requireNonNull(type);
+ this.action = action;
+ this.details = details;
}
/**
- * Gets the target of the action represented by this log entry. WARNING:
- * returns null if the content was RevisionDeleted and one does not
- * have access to the content.
- * @return the target of this log entry
+ * Gets the type of log that this entry is in.
+ * @return one of {@link Wiki#DELETION_LOG}, {@link Wiki#BLOCK_LOG}, etc.
* @since 0.08
*/
- public String getTarget()
+ public String getType()
{
- return target;
+ return type;
}
/**
- * Gets the timestamp of this log entry.
- * @return the timestamp of this log entry
+ * Gets a string description of the action performed, for example
+ * "delete", "protect", "overwrite", ... WARNING: returns null if the
+ * action was RevisionDeleted.
+ * @return the type of action performed
* @since 0.08
*/
- public OffsetDateTime getTimestamp()
+ public String getAction()
{
- return timestamp;
+ return action;
}
/**
@@ -6309,13 +7142,14 @@ public OffsetDateTime getTimestamp()
* USER_RENAME_LOG
* | The new username
* |
BLOCK_LOG
- * | new Object[] { boolean anononly, boolean nocreate, boolean
+ * | new Object[] { boolean anononly, boolean nocreate, boolean
* noautoblock, boolean noemail, boolean nousertalk, String duration }
* |
USER_RIGHTS_LOG
- * | The new user rights (String[])
+ * | The old ("oldgroups") and new ("newgroups") user rights,
+ * comma-separated
* |
PROTECTION_LOG
* | if action == "protect" or "modify" return the protection level
- * (int, -2 if unrecognized) if action == "move_prot" return
+ * (int, -2 if unrecognized) if action == "move_prot" return
* the old title, else null
* |
Others or RevisionDeleted
* | null
@@ -6329,7 +7163,7 @@ public OffsetDateTime getTimestamp()
* @return the details of the log entry
* @since 0.08
*/
- public Object getDetails()
+ public Map getDetails()
{
return details;
}
@@ -6342,44 +7176,21 @@ public Object getDetails()
@Override
public String toString()
{
- StringBuilder s = new StringBuilder("LogEntry[logid=");
- s.append(logid);
+ StringBuilder s = new StringBuilder(super.toString());
+ s.deleteCharAt(s.length() - 1);
s.append(",type=");
s.append(type);
s.append(",action=");
- s.append(action == null ? "[hidden]" : action);
- s.append(",user=");
- s.append(user == null ? "[hidden]" : user.getUsername());
- s.append(",timestamp=");
- s.append(timestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
- s.append(",target=");
- s.append(target == null ? "[hidden]" : target);
- s.append(",reason=\"");
- s.append(reason == null ? "[hidden]" : reason);
- s.append("\",details=");
- if (details instanceof Object[])
- s.append(Arrays.asList((Object[])details)); // crude formatting hack
- else
- s.append(details);
+ s.append(Objects.toString(action, "[DELETED]"));
+ s.append(",details=");
+ s.append(details);
s.append("]");
return s.toString();
}
/**
- * Compares this log entry to another one based on the recentness
- * of their timestamps.
- * @param other the log entry to compare
- * @return whether this object is equal to
- * @since 0.18
- */
- @Override
- public int compareTo(Wiki.LogEntry other)
- {
- return timestamp.compareTo(other.timestamp);
- }
-
- /**
- * Determines whether two LogEntries refer to the same event.
+ * Determines whether two LogEntries are equal based on the underlying
+ * {@linkplain Event#equals(Object) Event}, type and action.
* @param other some object to compare to
* @return (see above)
* @since 0.33
@@ -6387,12 +7198,28 @@ public int compareTo(Wiki.LogEntry other)
@Override
public boolean equals(Object other)
{
+ if (!super.equals(other))
+ return false;
if (!(other instanceof LogEntry))
return false;
LogEntry le = (LogEntry)other;
- return type.equals(le.type) && action.equals(le.action) &&
- user.equals(le.user) && target.equals(le.target) &&
- reason.equals(le.reason) && timestamp.equals(le.timestamp);
+ return Objects.equals(type, le.type)
+ && Objects.equals(action, le.action);
+ }
+
+ /**
+ * Computes a hashcode for this LogEntry based on the underlying
+ * {@linkplain Event#hashCode() Event}, type and action.
+ * @return (see above)
+ * @since 0.35
+ */
+ @Override
+ public int hashCode()
+ {
+ int hc = super.hashCode();
+ hc = 127 * hc + type.hashCode();
+ hc = 127 * hc + Objects.hashCode(action);
+ return hc;
}
}
@@ -6400,46 +7227,39 @@ public boolean equals(Object other)
* Represents a contribution and/or a revision to a page.
* @since 0.17
*/
- public class Revision implements Comparable
+ public class Revision extends Event
{
- private boolean minor, bot, rvnew;
- private String summary;
- private long revid, rcid = -1;
+ private final boolean minor, bot, rvnew;
+ private final String sha1;
+ private long rcid = -1;
private long previous = 0, next = 0;
- private OffsetDateTime timestamp;
- private String user;
- private String title;
- private String rollbacktoken = null;
- private int size = 0;
- private int sizediff = 0;
- private boolean summaryDeleted = false, userDeleted = false, contentDeleted = false;
+ private int size = 0, sizediff = 0;
private boolean pageDeleted = false;
/**
* Constructs a new Revision object.
* @param revid the id of the revision (this is a long since
- * {{NUMBEROFEDITS}} on en.wikipedia.org is now (January 2012) ~25%
- * of Integer.MAX_VALUE
+ * {{NUMBEROFEDITS}} on en.wikipedia.org is now (January 2018) ~38%
+ * of {@code Integer.MAX_VALUE}
* @param timestamp when this revision was made
+ * @param user the user making this revision (may be anonymous)
+ * @param comment the edit summary
+ * @param parsedcomment the edit summary, parsed into HTML
* @param title the concerned article
- * @param summary the edit summary
- * @param user the user making this revision (may be anonymous, if not
- * use User.getUsername())
+ * @param sha1 the SHA-1 hash of the revision
* @param minor whether this was a minor edit
* @param bot whether this was a bot edit
* @param rvnew whether this revision created a new page
* @param size the size of the revision
* @since 0.17
*/
- public Revision(long revid, OffsetDateTime timestamp, String title, String summary, String user,
- boolean minor, boolean bot, boolean rvnew, int size)
+ public Revision(long revid, OffsetDateTime timestamp, String user, String comment,
+ String parsedcomment, String title, String sha1, boolean minor, boolean bot,
+ boolean rvnew, int size)
{
- this.revid = revid;
- this.timestamp = timestamp;
- this.summary = summary;
+ super(revid, timestamp, user, Objects.requireNonNull(title), comment, parsedcomment);
+ this.sha1 = sha1;
this.minor = minor;
- this.user = user;
- this.title = title;
this.bot = bot;
this.rvnew = rvnew;
this.size = size;
@@ -6447,7 +7267,7 @@ public Revision(long revid, OffsetDateTime timestamp, String title, String summa
/**
* Fetches the contents of this revision.
- * @return the contents of the appropriate article at timestamp
+ * @return the contents of the appropriate article at timestamp
* @throws IOException if a network error occurs
* @throws IllegalArgumentException if page == Special:Log/xxx.
* @since 0.17
@@ -6455,176 +7275,125 @@ public Revision(long revid, OffsetDateTime timestamp, String title, String summa
public String getText() throws IOException
{
// logs have no content
- if (revid < 1L)
+ if (getID() < 1L)
throw new IllegalArgumentException("Log entries have no valid content!");
// TODO: returning a 404 here when revision content has been deleted
// is not a good idea.
if (pageDeleted) // FIXME: broken if a page is live, but has deleted revisions
{
- String url = query + "prop=deletedrevisions&drvprop=content&revids=" + revid;
- String temp = fetch(url, "Revision.getText");
+ Map getparams = new HashMap<>();
+ getparams.put("action", "query");
+ getparams.put("prop", "deletedrevisions");
+ getparams.put("drvprop", "content");
+ getparams.put("revids", String.valueOf(getID()));
+ String temp = makeApiCall(getparams, null, "Revision.getText");
+ detectUncheckedErrors(temp, null, null);
int a = temp.indexOf("", a) + 1;
+ a = temp.indexOf('>', a) + 1;
int b = temp.indexOf("", a); // tag not present if revision has no content
- log(Level.INFO, "Revision.getText", "Successfully retrieved text of revision " + revid);
+ log(Level.INFO, "Revision.getText", "Successfully retrieved text of revision " + getID());
return (b < 0) ? "" : temp.substring(a, b);
}
else
- {
- String url = base + encode(title, true) + "&oldid=" + revid + "&action=raw";
- String temp = fetch(url, "Revision.getText");
- log(Level.INFO, "Revision.getText", "Successfully retrieved text of revision " + revid);
- return temp;
- }
+ return Wiki.this.getText(null, new long[] { getID() }, -1).get(0);
}
/**
* Gets the rendered text of this revision.
* @return the rendered contents of the appropriate article at
- * timestamp
+ * timestamp
* @throws IOException if a network error occurs
* @throws IllegalArgumentException if page == Special:Log/xxx.
* @since 0.17
*/
public String getRenderedText() throws IOException
{
- // logs have no content
- if (revid < 1L)
+ if (getID() < 1L)
throw new IllegalArgumentException("Log entries have no valid content!");
-
- String temp;
- if (pageDeleted)
- {
- String url = query + "prop=deletedrevisions&drvprop=content&drvparse=1&revids=" + revid;
- temp = fetch(url, "Revision.getRenderedText");
- // TODO
- }
- else
- {
- String url = base + "&action=render&oldid=" + revid;
- temp = fetch(url, "Revision.getRenderedText");
- }
- log(Level.INFO, "Revision.getRenderedText", "Successfully retrieved rendered text of revision " + revid);
- return decode(temp);
+ Map content = new HashMap<>();
+ content.put("revision", this);
+ return Wiki.this.parse(content, -1, false);
}
/**
- * Returns true if the revision content is RevisionDeleted.
+ * Returns the SHA-1 hash (base 16, lower case) of the content of this
+ * revision, or {@code null} if the revision content is RevisionDeleted
+ * and we cannot access it.
+ *
+ * Warnings:
+ *
+ * - Not available through {@link #watchlist(RequestHelper)} or {@link
+ * #contribs(List, String, RequestHelper)}.
+ *
+ *
* @return (see above)
- * @since 0.31
+ * @since 0.35
*/
- public boolean isContentDeleted()
+ public String getSha1()
{
- return contentDeleted;
+ return sha1;
}
/**
- * Returns a HTML rendered diff table; see the table at the example.
- * Returns null for page creations, moves, protections and similar
- * dummy edits (
- * example).
- *
+ * Returns a HTML rendered diff table of this revision to other.
+ * See {@link #diff(Map, Map)} for full documentation.
+ *
* @param other another revision on the same page.
* @return the difference between this and the other revision
* @throws IOException if a network error occurs
+ * @throws SecurityException if this or the other revision is
+ * RevisionDeleted and the user lacks the necessary privileges
* @since 0.21
*/
public String diff(Revision other) throws IOException
{
- return diff(other.revid, "");
+ return Wiki.this.diff(Map.of("revision", this), Map.of("revision", other));
}
/**
- * Returns a HTML rendered diff table between this revision and the
- * given text. Useful for emulating the "show changes" functionality.
- * See the table at the text. Useful for emulating the "show changes"
+ * functionality. See the table at the example.
+ *
* @param text some wikitext
* @return the difference between this and the the text provided
* @throws IOException if a network error occurs
+ * @throws SecurityException if this or the other revision is
+ * RevisionDeleted and the user lacks the necessary privileges
* @since 0.21
*/
public String diff(String text) throws IOException
{
- return diff(0L, text);
+ return Wiki.this.diff(Map.of("revision", this), Map.of("text", text));
}
/**
- * Returns a HTML rendered diff table; see the table at the example.
- * Returns null for page creations, moves, protections and
- * similar dummy edits (example)
- * and pairs of revisions where there is no difference.
- *
- * @param oldid the oldid of a revision on the same page. {@link Wiki#NEXT_REVISION},
- * {@link Wiki#PREVIOUS_REVISION} and {@link Wiki#CURRENT_REVISION} can
- * be used here for obvious effect.
+ * Returns a HTML rendered diff table from this revision to the given
+ * oldid. See {@link #diff(Map, Map)} for full documentation.
+ *
+ * @param oldid the oldid of a revision on the same page. {@link
+ * Wiki#NEXT_REVISION}, {@link Wiki#PREVIOUS_REVISION} and {@link
+ * Wiki#CURRENT_REVISION} can be used here for obvious effect.
* @return the difference between this and the other revision
* @throws IOException if a network error occurs
+ * @throws SecurityException if this or the other revision is
+ * RevisionDeleted and the user lacks the necessary privileges
* @since 0.26
*/
public String diff(long oldid) throws IOException
{
- return diff(oldid, "");
- }
-
- /**
- * Fetches a HTML rendered diff table; see the table at the example.
- * Returns null for page creations, moves, protections and similar
- * dummy edits (
- * example) and pairs of revisions where there is no difference.
- *
- * @param oldid the id of another revision; (exclusive) or
- * @param text some wikitext to compare against
- * @return a difference between oldid or text or null if there is no
- * diff.
- * @throws IOException if a network error occurs
- * @since 0.21
- */
- protected String diff(long oldid, String text) throws IOException
- {
- StringBuilder temp = new StringBuilder();
- if (oldid == NEXT_REVISION)
- temp.append("&torelative=next");
- else if (oldid == CURRENT_REVISION)
- temp.append("&torelative=cur");
- else if (oldid == PREVIOUS_REVISION)
- temp.append("&torelative=prev");
- else if (oldid == 0L)
- {
- temp.append("&totext=");
- temp.append(text);
- }
- else
- {
- temp.append("&torev=");
- temp.append(oldid);
- }
-
- String line = post(apiUrl + "action=compare&fromrev=" + getRevid(), temp.toString(), "Revision.diff");
-
- // strip extraneous information
- if (line.contains(""))
- {
- int a = line.indexOf("", a) + 1;
- int b = line.indexOf("", a);
- return decode(line.substring(a, b));
- }
- else
- // tag has no content if there is no diff or the two
- // revisions are identical. In particular, the API does not
- // distinguish between:
- // https://en.wikipedia.org/w/index.php?title=Source_Filmmaker&diff=804972897&oldid=803731343 (no difference)
- // https://en.wikipedia.org/w/index.php?title=Dayo_Israel&oldid=738178354&diff=prev (dummy edit)
- return null;
+ Map from = new HashMap<>();
+ from.put("revision", this);
+ Map to = new HashMap<>();
+ to.put("revid", oldid);
+ return Wiki.this.diff(from, to);
}
/**
- * Determines whether this Revision is equal to another object.
+ * Determines whether this Revision is equal to another based on the
+ * underlying {@linkplain Event#equals(Object) Event}.
* @param o an object
* @return whether o is equal to this object
* @since 0.17
@@ -6632,20 +7401,29 @@ else if (oldid == 0L)
@Override
public boolean equals(Object o)
{
+ // Note to self: don't use SHA-1 until all API calls provide it.
+ if (!super.equals(o))
+ return false;
if (!(o instanceof Revision))
return false;
- return revid == ((Revision)o).revid;
+ // Revision rev = (Revision)o;
+ // return Objects.equals(sha1, rev.sha1);
+ return true;
}
/**
- * Returns a hash code of this revision.
+ * Returns a hash code of this revision based on the underlying
+ * {@linkplain Event#hashCode() Event}.
* @return a hash code
* @since 0.17
*/
@Override
public int hashCode()
{
- return (int)revid * 2 - Wiki.this.hashCode();
+ // Note to self: don't use SHA-1 until all API calls provide it.
+ int hc = super.hashCode();
+ hc = 127 * hc;
+ return hc;
}
/**
@@ -6671,12 +7449,15 @@ public boolean isBot()
}
/**
- * Determines whether this revision created a new page.
- * WARNING: Will return false for all revisions prior to 2007
- * (I think?) -- this is a MediaWiki problem.
- * WARNING: Returning true does not imply this is the bottommost
- * revision on the page due to histmerges.
- * WARNING: Not accessible through getPageHistory() -- a MW problem.
+ * Determines whether this revision created a new page.
+ *
+ * Warnings:
+ *
+ * - Returning {@code true} does not imply this is the bottommost
+ * revision on the page due to histmerges.
+ *
- Not available through {@link #getPageHistory(String, Wiki.RequestHelper)}
+ *
+ *
* @return (see above)
* @since 0.27
*/
@@ -6686,51 +7467,8 @@ public boolean isNew()
}
/**
- * Returns the edit summary for this revision, or null if the summary
- * was RevisionDeleted and you lack the necessary privileges.
- * @return the edit summary
- * @since 0.17
- */
- public String getSummary()
- {
- return summary;
- }
-
- /**
- * Returns true if the edit summary is RevisionDeleted.
- * @return (see above)
- * @since 0.30
- */
- public boolean isSummaryDeleted()
- {
- return summaryDeleted;
- }
-
- /**
- * Returns the user or anon who created this revision. You should
- * pass this (if not an IP) to getUser(String) to obtain a
- * User object. Returns null if the user was RevisionDeleted and you
- * lack the necessary privileges.
- * @return the user or anon
- * @since 0.17
- */
- public String getUser()
- {
- return user;
- }
-
- /**
- * Returns true if the user is RevisionDeleted.
- * @return (see above)
- * @since 0.30
- */
- public boolean isUserDeleted()
- {
- return userDeleted;
- }
-
- /**
- * Returns true if this revision is deleted (different from revdeleted).
+ * Returns {@code true} if this revision is deleted (not the same as
+ * RevisionDeleted).
* @return (see above)
* @since 0.31
*/
@@ -6739,37 +7477,6 @@ public boolean isPageDeleted()
return pageDeleted;
}
- /**
- * Returns the page to which this revision was made.
- * @return the page
- * @since 0.17
- */
- public String getPage()
- {
- return title;
- }
-
- /**
- * Returns the oldid of this revision. Don't confuse this with
- * rcid
- * @return the oldid (long)
- * @since 0.17
- */
- public long getRevid()
- {
- return revid;
- }
-
- /**
- * Gets the time that this revision was made.
- * @return the timestamp
- * @since 0.17
- */
- public OffsetDateTime getTimestamp()
- {
- return timestamp;
- }
-
/**
* Gets the size of this revision in bytes.
* @return see above
@@ -6781,7 +7488,8 @@ public int getSize()
}
/**
- * Returns the change in page size caused by this revision.
+ * Returns the change in page size caused by this revision. Not available
+ * through getPageHistory or getDeletedHistory.
* @return see above
* @since 0.28
*/
@@ -6798,22 +7506,8 @@ public int getSizeDiff()
@Override
public String toString()
{
- StringBuilder sb = new StringBuilder("Revision[oldid=");
- sb.append(revid);
- sb.append(",page=\"");
- sb.append(title);
- sb.append("\",user=");
- sb.append(user == null ? "[hidden]" : user);
- sb.append(",userdeleted=");
- sb.append(userDeleted);
- sb.append(",timestamp=");
- sb.append(timestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
- sb.append(",summary=\"");
- sb.append(summary == null ? "[hidden]" : summary);
- sb.append("\",summarydeleted=");
- sb.append(summaryDeleted);
- sb.append(",contentDeleted=");
- sb.append(contentDeleted);
+ StringBuilder sb = new StringBuilder(super.toString());
+ sb.deleteCharAt(sb.length() - 1);
sb.append(",minor=");
sb.append(minor);
sb.append(",bot=");
@@ -6826,25 +7520,10 @@ public String toString()
sb.append(previous);
sb.append(",next=");
sb.append(next);
- sb.append(",rollbacktoken=");
- sb.append(rollbacktoken == null ? "null" : rollbacktoken);
sb.append("]");
return sb.toString();
}
- /**
- * Compares this revision to another revision based on the recentness
- * of their timestamps.
- * @param other the revision to compare
- * @return whether this object is equal to
- * @since 0.18
- */
- @Override
- public int compareTo(Wiki.Revision other)
- {
- return timestamp.compareTo(timestamp);
- }
-
/**
* Gets the previous revision.
* @return the previous revision, or null if this is the first revision
@@ -6892,35 +7571,25 @@ public long getRcid()
}
/**
- * Sets a rollback token for this revision.
- * @param token a rollback token
- * @since 0.24
- */
- public void setRollbackToken(String token)
- {
- rollbacktoken = token;
- }
-
- /**
- * Gets the rollback token for this revision. Can be null, and often
- * for good reasons: cannot rollback or not top revision.
- * @return the rollback token
- * @since 0.24
+ * Gets a permanent URL to the human-readable version of this Revision.
+ * This URL uses index.php, not Special:Permanentlink for ease of adding
+ * other parameters.
+ * @return (see above)
+ * @since 0.35
*/
- public String getRollbackToken()
+ public String permanentUrl()
{
- return rollbacktoken;
+ return getIndexPhpUrl() + "?oldid=" + getID();
}
/**
- * Reverts this revision using the rollback method. See
- * {@link Wiki#rollback(org.wikipedia.Wiki.Revision)}.
- *
+ * Reverts this revision using the rollback method.
+ *
* @throws IOException if a network error occurs
- * @throws CredentialNotFoundException if not logged in or user is not
- * an admin
+ * @throws SecurityException if the user lacks the privileges to rollback
* @throws CredentialExpiredException if cookies have expired
* @throws AccountLockedException if the user is blocked
+ * @see Wiki#rollback(org.wikipedia.Wiki.Revision)
* @since 0.19
*/
public void rollback() throws IOException, LoginException
@@ -6929,174 +7598,586 @@ public void rollback() throws IOException, LoginException
}
/**
- * Reverts this revision using the rollback method. See
- * {@link Wiki#rollback(org.wikipedia.Wiki.Revision)}.
- *
+ * Reverts this revision using the rollback method.
+ *
* @param bot mark this and the reverted revision(s) as bot edits
* @param reason (optional) a custom reason
* @throws IOException if a network error occurs
- * @throws CredentialNotFoundException if not logged in or user is not
- * an admin
+ * @throws SecurityException if the user lacks the privileges to rollback
* @throws CredentialExpiredException if cookies have expired
* @throws AccountLockedException if the user is blocked
+ * @see Wiki#rollback(org.wikipedia.Wiki.Revision)
* @since 0.19
*/
- public void rollback(boolean bot, String reason) throws IOException, LoginException
+ public void rollback(boolean bot, String reason) throws IOException, LoginException
+ {
+ Wiki.this.rollback(this, bot, reason);
+ }
+ }
+
+ /**
+ * Vehicle for stuffing standard optional parameters into Wiki queries.
+ * {@code RequestHelper} objects are reusable. The following example
+ * fetches articles from the back of the new pages queue on the
+ * English Wikipedia.
+ *
+ * {@code
+ * Wiki.RequestHelper rh = enWiki.new RequestHelper()
+ * .inNamespaces(Wiki.MAIN_NAMESPACE)
+ * .reverse();
+ * List newpages = enWiki.newPages(rh);
+ * }
+ *
+ * @since 0.36
+ */
+ public class RequestHelper
+ {
+ private String title;
+ private String byuser;
+ private OffsetDateTime earliest, latest;
+ private int[] localns = new int[0];
+ private boolean reverse = false;
+ private String notbyuser;
+ private String tag;
+ private Map options;
+ private String requestType;
+ private int limit = -1;
+
+ /**
+ * Creates a new RequestHelper.
+ */
+ public RequestHelper()
+ {
+ }
+
+ /**
+ * Limits query results to Events occuring on the given title. If a
+ * query mandates a title parameter (e.g. {@link #getPageHistory(String,
+ * RequestHelper)}, don't use this. Use the parameter in the query
+ * method instead.
+ * @param title a page title
+ * @return this RequestHelper
+ */
+ public RequestHelper byTitle(String title)
+ {
+ this.title = (title == null) ? null : normalize(title);
+ return this;
+ }
+
+ /**
+ * Limits query results to Events triggered by the given user. If a query
+ * mandates a user parameter (e.g. {@link #contribs(List, String, RequestHelper)},
+ * don't use this. Use the parameter in the query method instead.
+ * @param byuser some username or IP address
+ * @return this RequestHelper
+ */
+ public RequestHelper byUser(String byuser)
+ {
+ this.byuser = (byuser == null) ? null : normalize(byuser);
+ return this;
+ }
+
+ /**
+ * Limit results to be within this date range.
+ * @param earliest the lower (earliest) date bound, use {@code null} to
+ * not set one
+ * @param latest the higher (latest) date bound, use {@code null} to
+ * not set one
+ * @throws IllegalArgumentException if {@code earliest.isAfter(latest)}
+ * @return this RequestHelper
+ */
+ public RequestHelper withinDateRange(OffsetDateTime earliest, OffsetDateTime latest)
+ {
+ if (earliest != null && latest != null && earliest.isAfter(latest))
+ throw new IllegalArgumentException("Earliest date must be before latest date!");
+ this.earliest = earliest;
+ this.latest = latest;
+ return this;
+ }
+
+ /**
+ * Limits query results to the given namespaces.
+ * @param ns a list of namespaces
+ * @return this RequestHelper
+ */
+ public RequestHelper inNamespaces(int... ns)
+ {
+ localns = ns;
+ return this;
+ }
+
+ /**
+ * Should we perform this query in reverse order (earliest first).
+ * @param reverse whether to reverse this query
+ * @return this RequestHelper
+ */
+ public RequestHelper reverse(boolean reverse)
+ {
+ this.reverse = reverse;
+ return this;
+ }
+
+ /**
+ * Limits query results to {@link Event Events} that have been tagged
+ * with the given tag.
+ * @param tag a change tag
+ * @return this RequestHelper
+ */
+ public RequestHelper taggedWith(String tag)
+ {
+ this.tag = tag;
+ return this;
+ }
+
+ /**
+ * Limits query results to Events NOT triggered by the given user.
+ * @param notbyuser some username or IP address to exclude
+ * @return this RequestHelper
+ */
+ public RequestHelper notByUser(String notbyuser)
+ {
+ this.notbyuser = (notbyuser == null) ? null : normalize(notbyuser);
+ return this;
+ }
+
+ /**
+ * Return no more than the given number of results. Overrides {@linkplain
+ * #setQueryLimit(int) global limits}. Supply a negative integer to use
+ * global limits.
+ * @param limit a positive integer
+ * @return this RequestHelper
+ */
+ public RequestHelper limitedTo(int limit)
+ {
+ this.limit = limit;
+ return this;
+ }
+
+ /**
+ * Filters a set of returned results using the given options. Please
+ * check calling method documentation for supported options.
+ *
+ *
+ * When filtering revisions, available keys may include "minor", "top",
+ * "new", "bot", "anon", "redirect", "patrolled" and "unread" for
+ * vanilla MediaWiki. Extensions may define their own. For instance,
+ * {@code rcoptions = { minor = true, anon = false, patrolled = false}}
+ * returns all minor edits from logged in users that aren't patrolled.
+ * Setting "patrolled" limits results to no older than retention in
+ * the recentchanges
+ * table.
+ *
+ * @param options the options to filter by
+ * @return this RequestHelper
+ */
+ public RequestHelper filterBy(Map options)
+ {
+ this.options = options;
+ return this;
+ }
+
+ /**
+ * Sets the prefix of API request parameters (the XX in XXlimit, XXdir,
+ * XXnamespace and so forth). Internal use only.
+ * @param prefix the prefix to use (must not be null)
+ */
+ protected void setRequestType(String prefix)
+ {
+ requestType = Objects.requireNonNull(prefix);
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the title to get
+ * events for, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addTitleParameter()
+ {
+ if (title != null)
+ return Map.of(requestType + "title", title);
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the user to filter
+ * returned events by, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addUserParameter()
+ {
+ if (byuser != null)
+ return Map.of(requestType + "user", byuser);
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the dates to start
+ * and end enumeration, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addDateRangeParameters()
+ {
+ // https://phabricator.wikimedia.org/T16449
+ Map temp = new HashMap<>();
+ OffsetDateTime odt = reverse ? earliest : latest;
+ if (odt != null)
+ temp.put(requestType + "start",
+ // https://www.mediawiki.org/wiki/Timestamp
+ // https://github.com/MER-C/wiki-java/issues/170
+ odt.withOffsetSameInstant(ZoneOffset.UTC)
+ .truncatedTo(ChronoUnit.MICROS)
+ .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
+ odt = reverse ? latest : earliest;
+ if (odt != null)
+ temp.put(requestType + "end",
+ // https://www.mediawiki.org/wiki/Timestamp
+ // https://github.com/MER-C/wiki-java/issues/170
+ odt.withOffsetSameInstant(ZoneOffset.UTC)
+ .truncatedTo(ChronoUnit.MICROS)
+ .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
+ return temp;
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the namespaces to limit
+ * this query to, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addNamespaceParameter()
+ {
+ if (localns.length != 0)
+ return Map.of(requestType + "namespace", constructNamespaceString(localns));
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a HTTP request parameter instructing the API to reverse the
+ * query, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addReverseParameter()
+ {
+ return Map.of(requestType + "dir", reverse ? "newer" : "older");
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the tag to limit
+ * returned events to, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addTagParameter()
+ {
+ if (tag != null)
+ return Map.of(requestType + "tag", tag);
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a HTTP request parameter containing the user to exclude
+ * when returning events, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addExcludeUserParameter()
+ {
+ if (notbyuser != null)
+ return Map.of(requestType + "excludeuser", notbyuser);
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns HTTP request parameter(s) containing flags to filter returned
+ * revisions by, or an empty map if not wanted.
+ * @return (see above)
+ */
+ protected Map addShowParameter()
+ {
+ Map temp = new HashMap<>();
+ if (options != null && !options.isEmpty())
+ {
+ // deal with MW API annoyance for action=query&list=watchlist - see watchlist(rh)
+ Boolean top = null;
+ if (requestType.equals("wl"))
+ {
+ top = options.remove("top");
+ if (Boolean.TRUE.equals(top))
+ temp.put("wlallrev", "1");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ options.forEach((key, value) ->
+ {
+ if (Boolean.FALSE.equals(value))
+ sb.append('!');
+ sb.append(key);
+ sb.append("|");
+ });
+ temp.put(requestType + "show", sb.substring(0, sb.length() - 1));
+
+ if (top != null) // put it back
+ options.put("top", top);
+ }
+ return temp;
+ }
+
+ /**
+ * Returns the number of results the query should be limited to. If not
+ * present, use {@linkplain #setQueryLimit(int) global limits}.
+ * @return (see above)
+ */
+ public int limit()
{
- Wiki.this.rollback(this, bot, reason);
+ return limit;
}
}
// INTERNALS
+ /**
+ * Performs a vectorized action=query&prop=X type API query
+ * over titles.
+ * @param queryPrefix the request type prefix (e.g. "pl" for prop=links)
+ * @param getparams a bunch of parameters to send via HTTP GET
+ * @param titles a list of titles
+ * @param caller the name of the calling method
+ * @param limit fetch no more than this many results
+ * @param parser a BiConsumer that parses the XML returned by the MediaWiki
+ * API into things we want, dumping them into the given List
+ * @return a list of results, where each element corresponds to the element
+ * at the same index in the input title list
+ * @since 0.36
+ * @throws IOException if a network error occurs
+ */
+ protected List> makeVectorizedQuery(String queryPrefix, Map getparams,
+ List titles, String caller, int limit, BiConsumer> parser) throws IOException
+ {
+ // copy because normalization and redirect resolvers overwrite
+ List titles2 = new ArrayList<>(titles);
+ List |