package votorola.a.position; // Copyright 2010-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.io.*; import java.net.*; 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.count.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; /** A particular revision of the single page at the core of a position. * * @see Category:Position */ public interface CoreRevision extends PositionalRevision { /** Finishes the construction of this CoreRevision and sets {@linkplain * #isFullyConstructed() isFullyConstructed} true. * * @param contextPersonName the name of the person from whose viewpoint the * revision will be viewed (context person), or null if there is no context * person. * @param countSource for possible use during this call. It is never used if * there is no context person. * * @throws IllegalStateException if called a second time. */ public @ThreadRestricted("constructor") void init( PollwikiVS wiki, String contextPersonName, CountSource countSource ) throws IOException; // - C o r e - R e v i s i o n -------------------------------------------------------- /** Adds to the list the revision path from this core revision (inclusive) to the * terminal {@linkplain #draft() draft} (inclusive). * * @return the same list. */ public List addDraftRevisionPath( List path ); /** Constructs a view of this core revision suited for viewing in the context of the * specified person, or returns this same (==) core revision if it is already suited. */ public CoreRevision contextView( final String contextPersonName ); /** The position draft. This may be null if the revision is incompletely constructed. */ public DraftRevision draft(); /** Answers whether the construction of this CoreRevision is complete. * * @see #init(PollwikiVS,String,CountSource) */ public boolean isFullyConstructed(); // ==================================================================================== /** CoreRevision utilities. */ public @ThreadSafe static final class U { private U() {} private static final Logger logger = LoggerX.i( CoreRevision.U.class ); /** @param b a string builder comprising 'titles=TITLES' or 'revids=REVS'. */ private static List partialFromQuerySpec( final StringBuilder b, final List toCores, final Class componentPipeClass, final PollwikiVS wiki, List userXList, final boolean toContinue ) throws IOException { b.insert( 0, "/api.php?format=xml&action=query&" ); b.insert( 0, wiki.scriptURI().toASCIIString() ); b.append( '&' ).append( read_QUERY ); final URL queryURL = new URL( b.toString() ); logger.fine( "querying pollwiki for position cores: " + queryURL ); final Spool spool = new Spool1(); try { final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); for( ;; ) { final CoreRevision core; try{ core = readPartial( xml, wiki, componentPipeClass ); } catch( final MediaWiki.NoSuchItem x ) { if( toContinue ) continue; else throw x; } catch( final IOException x ) { if( userXList == ThrowableX.ENLIST_NONE // == is correct here || !(x instanceof UserInformative )) throw x; userXList = ThrowableX.listedThrowable( x, userXList ); continue; } if( core == null ) break; toCores.add( core ); } } catch( final XMLStreamException x ) { throw new IOException( x ); } finally{ spool.unwind(); } return userXList; } // -------------------------------------------------------------------------------- /** Partially constructs the lastest revision of each specified core page and * appends it to the provided list. Constructs LocalDraftRevisions fully and * others partially. Appends revisions in an unspecified order that might not * correspond to the order of elements in pageNameList. * * @param toCores the list to which the partially constructed cores are to be * appended. * @param componentPipeClass the class of component pipe revisions to * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for * neither. * @param userXList a pre-constructed list to which any user actionable * exceptions are to be appended, or null if none was pre-constructed. * Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force * the immediate throwing of such exceptions. * @param toContinue whether to continue in the event of a missing page * (true), or instead to throw a NoSuchPage exception (false). * * @return the same userXList if there was one; otherwise null if no * exceptions were listed; otherwise a newly constructed list. * * @throws MediaWiki.NoSuchPage if toContinue is false and a page is missing. */ public static List partialFromPageNames( final List pageNameList, final List toCores, final Class componentPipeClass, final PollwikiVS wiki, final List userXList, final boolean toContinue ) throws IOException { final int pN = pageNameList.size(); if( pN == 0 ) return userXList; // nothing to do final StringBuilder b = new StringBuilder(); b.append( "titles=" ); for( int p = 0;; ) // append "name1|name2|name3|" ... { b.append( UriComponent.encode( pageNameList.get(p).toString(), UriComponent.Type.QUERY_PARAM )); ++p; if( p == pN ) break; b.append( "%7C" ); // vertical bar (|) } return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList, toContinue ); } /** Partially constructs the specified core revisions and appends them to the * provided list. Constructs LocalDraftRevisions fully and others partially. * Appends revisions in an unspecified order that might not correspond to the * order of elements in revList. * * @param toCores the list to which the partially constructed cores are to be * appended. * @param componentPipeClass the class of component pipe revisions to * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for * neither. * @param userXList a pre-constructed list to which any user actionable * exceptions are to be appended, or null if none was pre-constructed. * Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force * the immediate throwing of such exceptions. * @param toContinue whether to continue in the event of a non-existent * revision (true), or instead to throw a NoSuchRev exception (false). * * @return the same userXList if there was one; otherwise null if no * exceptions were listed; otherwise a newly constructed list. * * @throws MediaWiki.NoSuchRev if toContinue is false and a revision does not * exist. */ public static List partialFromRevs( final List revList, final List toCores, final Class componentPipeClass, final PollwikiVS wiki, final List userXList, final boolean toContinue ) throws IOException { final int rN = revList.size(); if( rN == 0 ) return userXList; // nothing to do final StringBuilder b = new StringBuilder(); b.append( "revids=" ); for( int r = 0;; ) // append "rev1|rev2|rev3|" ... { b.append( revList.get(r) ); ++r; if( r == rN ) break; b.append( "%7C" ); // vertical bar (|) } return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList, toContinue ); } /** Attempts to fully construct the {@linkplain #contextView(String) context view} * of a CoreRevision from a page query. * * @param xml a reader for the response to a page query in which at least one * 'page' element might remain to be read. The query must cover prop * 'info|revisions' (in order) and rvprop 'content', where the content is * for section 0 only. See for example {@linkplain #read_QUERY * read_QUERY}. * @param componentPipeClass the class of component pipe revisions to * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for * neither. * @param contextPersonName the name of the person from whose viewpoint the * revision will be viewed (context person), or null if there is no context * person. * @param countSource for possible use during this call. It is never used if * there is no context person. * * @return the context view of the fully constructed instance, or null if no * 'page' element remains to be read. * * @throws MediaWiki.NoSuchItem if the page or revision (whichever is * requested) does not exist. */ public static CoreRevision read( final XMLStreamReader xml, final PollwikiVS wiki, final Class componentPipeClass, final String contextPersonName, final CountSource countSource ) throws IOException, XMLStreamException { CoreRevision core = readPartial( xml, wiki, componentPipeClass ); if( core != null ) { core.init( wiki, contextPersonName, countSource ); core = core.contextView( contextPersonName ); } return core; } /** The encoded form of the minimal query parameters required for the {@linkplain * #read(XMLStreamReader,PollwikiVS,Class,String,CountSource) read} and * {@linkplain #readPartial(XMLStreamReader,PollwikiVS,Class) readPartial} * constructors. */ public static final String read_QUERY = "prop=info%7Crevisions&rvprop=content%7Cids&rvsection=0"; /** Attempts to partially construct a CoreRevision from a page query. Yields a * full construction for a LocalDraftRevision, a partial one for any other type, * and nothing at all if the response is exhausted. * * @param xml a reader for the response to a page query in which at least one * 'page' element might remain to be read. The query must cover prop * 'info|revisions' (in order) and rvprop 'content', where the content is * for section 0 only. See for example {@linkplain #read_QUERY * read_QUERY}. * @param componentPipeClass the class of component pipe revisions to * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for * neither. * * @return the partially constructed instance, or null if no 'page' element * remains to be read. * * @throws MediaWiki.NoSuchItem if the page or revision (whichever is * requested) does not exist. */ public static CoreRevision readPartial( final XMLStreamReader xml, final PollwikiVS wiki, final Class componentPipeClass ) throws IOException, XMLStreamException { // // // // while( xml.hasNext() ) { xml.next(); if( !xml.isStartElement() ) continue; if( "page".equals( xml.getLocalName() )) { MediaWiki.testPage_missing( xml ); /* when querying by 'titles'; else if querying by 'revids' see 'badrevids' below */ final int revLatest = Integer.parseInt( xml.getAttributeValue( /*ns*/null, "lastrevid" )); final int pageID = Integer.parseInt( xml.getAttributeValue( /*ns*/null, "pageid" )); final String pageName = xml.getAttributeValue( /*ns*/null, "title" ); String content0 = null; int rev = -1; page: while( xml.hasNext() ) { xml.next(); if( xml.isEndElement() && "page".equals( xml.getLocalName() )) break page; // one page, that's all if( xml.isStartElement() ) { final String name = xml.getLocalName(); if( "rev".equals( name )) { rev = Integer.parseInt( xml.getAttributeValue( /*ns*/null, "revid" )); content0 = xml.getElementText(); // moves state to // break page; // last prop requested /// no actual need to be dependent on request order, break on } // else if( "categories".equals( name )) // { // categories: while( xml.hasNext() ) // { // xml.next(); // if( xml.isEndElement() && "categories".equals( xml.getLocalName() )) // { // break categories; // } // // if( xml.isStartElement() && "cl".equals( xml.getLocalName() )) // { // if( "Category:Draft pointer".equals( xml.getAttributeValue( // /*ns*/null, "title" ))) // { // isDraftPointer = true; // break categories; // } // } // } // } /// attempts to optimize pointer detection by restricting pattern /// searches (below) to categorized pages, but fails because category /// is tied to page not rev } } if( content0 == null || rev == -1 ) { throw new MediaWiki.MalformedResponse( "missing 'rev' element" ); } if( componentPipeClass != null ) { final MatchResult r = MediaWiki.parsePageNameS( pageName ); if( r == null ) { throw new MediaWiki.MalformedResponse( "malformed page name:" + pageName ); } if( /*subpage*/r.group(3) == null ) { final String username = r.group( 2 ); if( wiki.pipeRecognizer().isPipeName( username )) { final Matcher m = ComponentPipeRevision.TEMPLATE_CALL_PATTERN.matcher( content0 ); if( m.find() ) { if( ComponentPipeRevision1.class.equals( componentPipeClass )) { return new ComponentPipeRevision1( wiki, pageID, pageName, rev, revLatest, /*nomineeName*/MediaWiki.normalUsername(m.group(1)), username, /*pollName*/m.group(2) ); } if( ComponentPipeRevisionL.class.equals( componentPipeClass )) { return new ComponentPipeRevisionL( wiki, pageID, pageName, rev, revLatest, /*nomineeName*/MediaWiki.normalUsername(m.group(1)), username, /*pollName*/m.group(2) ); } throw new IllegalArgumentException( "componentPipeClass" ); } } } } // not a component pipe, maybe a draft pointer: final String remotePageName; final String siteName; pointer: { Matcher m; m = PointerRevision.TEMPLATE_CALL_PATTERN_2.matcher( content0 ); if( m.find() ) { remotePageName = m.group( 1 ); siteName = m.group( 2 ); break pointer; } m = PointerRevision.TEMPLATE_CALL_PATTERN_1.matcher( content0 ); if( m.find() ) { assert "metagovernment.org".equals( m.group( 1 )); siteName = "Stuff:Metagovernment/wiki"; remotePageName = m.group( 2 ); break pointer; } m = PointerRevision.EXTERNAL_LINK_PATTERN.matcher( content0 ); if( m.find() ) { assert "metagovernment.org".equals( m.group( 1 )); siteName = "Stuff:Metagovernment/wiki"; remotePageName = MediaWiki.demiDecodedPageName( UriComponent.decode( m.group(2), UriComponent.Type.QUERY_PARAM )); break pointer; } // neither a component pipe nor a draft pointer, so: return new LocalDraftRevision( wiki, pageID, pageName, rev, revLatest, content0 ); } final PageProperty pDesign = new PageProperty( "Design" ); final PageProperty pScriptURL = new PageProperty( "Script URL" ); final PageProperty pURL = new PageProperty( "URL" ); final PagePropertyReader in = new PagePropertyReader( wiki.cache(), siteName, pDesign, pScriptURL, pURL ); try { in.readAllRequested(); final String design = pDesign.getValue(); if( !"Stuff:MediaWiki".equals( design )) { throw new PointerRevision.MalformedContent( "unsupported drafting design: " + design, wiki, rev ); } final String loc = pURL.getValue(); final String scriptLoc = pScriptURL.getValue(); if( loc == null || scriptLoc == null ) { throw new PointerRevision.MalformedContent( "drafting site '" + siteName + "' missing property '" + pScriptURL.name() + "' or '" + pURL.name() + '\'', wiki, rev ); } return new PointerRevision( wiki, pageID, pageName, rev, revLatest, remotePageName, new URI(loc), new URI(scriptLoc), content0 ); } catch( final URISyntaxException x ) { throw new PointerRevision.MalformedURL( x, wiki, rev ); } finally { in.close(); } } MediaWiki.test_badrevids( xml ); /* when querying by 'revids'; else if querying by 'titles' see testPage_missing above */ MediaWiki.test_error( xml ); } return null; } } }