package votorola.a.position; // Copyright 2011-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import com.sun.jersey.api.uri.UriComponent; import java.net.*; import java.io.*; import java.util.*; import java.util.logging.*; import votorola.g.logging.*; import java.util.regex.*; import javax.xml.stream.*; import votorola.a.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; /** A particular revision of a draft pointer. * * @see Category:Draft pointer */ public final @ThreadSafe class PointerRevision extends PageRevision1 implements CoreRevision { /** Partically constructs a PointerRevision for {@linkplain #init(RemoteDraftRevision) * init} to finish. * * @see #pageID() * @param _pageName it will be normalized. * @see #rev() * @see #revLatest() * @param _draftName it will be normalized. * @see #remoteWikiURI() * @see #remoteWikiScriptURI() * @param _content0 the wikitext of the revision including at least section 0. */ PointerRevision( final PollwikiVS wiki, int _pageID, String _pageName, int _rev, int _revLatest, String _draftName, URI _remoteWikiURI, URI _remoteWikiScriptURI, String _content0 ) throws PositionIDPair.MalformedPageName, PipeRevision.MalformedContent { super( wiki.scriptURI(), _pageID, _pageName, _rev, _revLatest, wiki.uri().toASCIIString(), wiki.maybeUgly() ); draftName = MediaWiki.demiDecodedPageName( _draftName ); // normalize positionID = PositionIDPair.newID( wiki, _pageName, _rev, _content0 ); remoteWikiScriptURI = _remoteWikiScriptURI; remoteWikiURI = _remoteWikiURI; remoteWikiURI_maybeUgly = MediaWiki.MAYBE_UGLY_URL_PATTERN.matcher( remoteWikiURI.toASCIIString() ).matches(); } /** Finishes the construction of this PointerRevision and sets {@linkplain * #isFullyConstructed() isFullyConstructed} true. * * @see #draft() * * @throws IllegalStateException if called a second time. */ public @ThreadRestricted("constructor") void init( RemoteDraftRevision _draft ) { if( isFullyConstructed ) throw new IllegalStateException(); // per API contract of this method, and init(wiki) draft = _draft; // if( draft == null ) throw new NullPointerException(); // fail fast /// done here: if( !draft.pageName().equals( draftName )) { throw new IllegalArgumentException( "expecting draft '" + draftName + "', recieved: " + draft.pageName() ); } isFullyConstructed = true; } public @ThreadRestricted("constructor") void init( final PollwikiVS wiki, String _contextPersonName, votorola.a.count.CountSource _countSource ) throws IOException { final LinkedList pointers = new LinkedList<>(); // cannot use singleton here, as initConsume modifies it pointers.add( PointerRevision.this ); initConsume( pointers, wiki, remoteWikiScriptURI, /*toContinue*/false ); // calls init(draft) which meets API contract regarding duplicate call } /** Attempts to finish constructing the specifed pointer revisions by constructing * their remote drafts, each to the latest revision. Call once only for each pointer * revision. * * @param pointers the list of pointer revisions from which the fully constructed * ones are to be removed. * @param toContinue whether to continue in the event of a missing draft and to * leave the incompletely constructed pointer in the list with a null * {@linkplain #draft() draft} (true), or to throw a NoSuchPage exception * (false). * * @throws MediaWiki.NoSuchPage if toContinue is false and a remote draft is * missing. */ public static @ThreadRestricted("constructor") void initConsume( final List pointers, final PollwikiVS wiki, final URI remoteWikiScriptURI, final boolean toContinue ) throws IOException { // cf. DraftPair.newDraftPair(DiffKeyParse,..) "Initialize any pointers..." if( pointers.size() == 0 ) return; // nothing to do final StringBuilder b = new StringBuilder(); b.append( remoteWikiScriptURI ); b.append( "/api.php?format=xml&action=query&titles=" ); for( final Iterator p = pointers.iterator();; ) { b.append( UriComponent.encode( p.next().draftName(), UriComponent.Type.QUERY_PARAM )); if( !p.hasNext() ) break; b.append( "%7C" ); // vertical bar (|) } b.append( "&prop=info" ); final URL queryURL = new URL( b.toString() ); logger.fine( "querying remote wiki for drafts: " + queryURL ); final Spool spool = new Spool1(); try { final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); for( ;; ) { final PageRevision remotePage; try { remotePage = PageRevision1.readPageRevision( remoteWikiScriptURI, xml, /*toLatest*/true ); } catch( final MediaWiki.NoSuchPage x ) { if( toContinue ) continue; throw newMissingRemoteDraft( x, pointers, wiki ); } if( remotePage == null ) break; // response order unpredictable, search for corresponding pointer(s): final String remotePageName = remotePage.pageName(); final Iterator p = pointers.iterator(); while( p.hasNext() ) { final PointerRevision pointer = p.next(); if( pointer.draftName().equals( remotePageName )) { p.remove(); // speed subsequent searches, meet API contract pointer.init( new RemoteDraftRevision( pointer, remotePage.pageID(), remotePageName, remotePage.rev(), remotePage.revLatest() )); // keep searching pointers, others may point to same remotePage } } } } catch( final XMLStreamException x ) { throw new IOException( x ); } finally{ spool.unwind(); } assert toContinue || pointers.size() == 0; } // ------------------------------------------------------------------------------------ /** The page name of the remote draft including the namespace. */ public String draftName() { return draftName; } private final String draftName; /** The search pattern for an external link to a remote draft in an obsolete draft * pointer page. If the pattern matches, it splits the name into groups (1) domain * name (always "metagovernment.org") and (2) page name of remote draft encoded as a * URL parameter value ("User:Joe/my_draft"). * * @see Category:Draft_pointer&oldid=4015 */ static final Pattern EXTERNAL_LINK_PATTERN = Pattern.compile( "(metagovernment\\.org)/\\S+/index\\.php[0-9]?\\?(?:\\S+&)?title=(\\S+)(?:&|\\s)" ); // SITE DOMAIN REMOTE DRAFT private static final Logger logger = LoggerX.i( PointerRevision.class ); /** The base URI for script execution in the remote wiki, without a trailing slash * (/). * * @see PageRevision#wikiScriptURI() * @see Template:Drafting site properties */ public URI remoteWikiScriptURI() { return remoteWikiScriptURI; } private final URI remoteWikiScriptURI; /** The base URI for requesting pages from the remote wiki, without a trailing slash * (/). This is either the standard access URI ending in something like "index.php" * for example, or an alias for it. * * @see Template:Drafting site properties */ public URI remoteWikiURI() { return remoteWikiURI; } private final URI remoteWikiURI; /** Answers whether the remote wiki URI might be based on the standard access URL * ending in "index.php" for example, or is definitely based on an alias. In the * latter case $wgUsePathInfo may be assumed true. */ public boolean remoteWikiURI_maybeUgly() { return remoteWikiURI_maybeUgly; } private final boolean remoteWikiURI_maybeUgly; /** The search pattern for a draft pointer template call in an obsolete draft pointer * page. If the pattern matches, it splits the name into groups based on the * template parameter values (1) domain name (always "metagovernment.org") and (2) * full page name of remote draft (e.g. "User:Joe/my draft"). * * * @see Category:Draft_pointer&oldid=5351 */ static final Pattern TEMPLATE_CALL_PATTERN_1 = Pattern.compile( "\\{\\s*\\{\\s*draft pointer\\s*" + "\\|\\s*index\\s*=\\s*http://\\S*(metagovernment\\.org)/\\S+/index\\.php[0-9]?\\s*" // SITE DOMAIN + "\\|\\s*page\\s*=\\s*([^=}]+?)\\s*" // REMOTE DRAFT + "\\}\\s*\\}" ); /** The search pattern for a draft pointer template call in a draft pointer page. If * the pattern matches, it splits the name into groups based on the template * parameter values (1) full page name of remote draft (e.g. "User:Joe/my_draft") and * (2) full page name of drafting site ("Stuff:Metagovernment/wiki"). This method of * extracting properties from a revision is required because SemanticMediawiki's data * store does not attach properties to revisions, only to pages. * * @see Category:Draft_pointer */ static final Pattern TEMPLATE_CALL_PATTERN_2 = Pattern.compile( "\\{\\s*\\{\\s*draft pointer\\s*" + "\\|\\s*page\\s*=\\s*([^=}]+?)\\s*" // REMOTE DRAFT + "\\|\\s*site\\s*=\\s*([^=}]+?)\\s*" // SITE PAGE + "\\}\\s*\\}" ); // - C o r e - R e v i s i o n -------------------------------------------------------- public List addDraftRevisionPath( final List path ) { path.add( rev() ); path.add( draft.rev() ); return path; } public CoreRevision contextView( String _contextPersonName ) { return PointerRevision.this; } public RemoteDraftRevision draft() { return draft; } private RemoteDraftRevision draft; // final when initialized /** @see #init(RemoteDraftRevision) */ public boolean isFullyConstructed() { return isFullyConstructed; } private boolean isFullyConstructed; // - P o s i t i o n a l - R e v i s i o n -------------------------------------------- public IDPair person() { return positionID.person(); } public String pollName() { return positionID.pollName(); } // ==================================================================================== /** Thrown when a request cannot be met because the content of a page is not in the * form required for a draft pointer. */ static @ThreadSafe class MalformedContent extends IOException implements UserInformative { MalformedContent( final String message, final PollwikiVS wiki, final int rev ) { super( message + " | " + MediaWiki.revLoc(wiki.scriptURI(),rev) ); } MalformedContent( final String message, Throwable _cause, final PollwikiVS wiki, final int rev ) { super( message + " | " + MediaWiki.revLoc(wiki.scriptURI(),rev), _cause ); } MalformedContent( Throwable _cause, final PollwikiVS wiki, final int rev ) { super( MediaWiki.revLoc(wiki.scriptURI(),rev), _cause ); } } // ==================================================================================== /** Thrown when a request cannot be met because a draft pointer encodes an external * link with a malformed URL. */ static @ThreadSafe final class MalformedURL extends MalformedContent { MalformedURL( Throwable _cause, PollwikiVS _wiki, int _rev ) { super( _cause, _wiki, _rev ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Constructs an appropriate exception to throw when a pointer points to a * non-existent remote draft. * * @param x the original exception that signaled the missing draft. * @param pointers a list of pointers including the broken one. */ private static IOException newMissingRemoteDraft( final MediaWiki.NoSuchPage x, final List pointers, final PollwikiVS wiki ) { final String remotePageName = x.pageName(); // find corresponding pointer: PointerRevision brokenPointer = null; for( PointerRevision p: pointers ) { if( p.draftName().equals( remotePageName )) { brokenPointer = p; break; } } if( brokenPointer == null ) { assert false; return x; } return new MalformedContent( "points to non-existent draft page", x, wiki, brokenPointer.rev() ); } private final PositionIDPair positionID; }