001package 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. 002 003import com.sun.jersey.api.uri.UriComponent; 004import java.io.*; 005import java.net.*; 006import java.util.*; 007import java.util.logging.*; import votorola.g.logging.*; 008import java.util.regex.*; 009import javax.xml.stream.*; 010import votorola.a.*; 011import votorola.a.count.*; 012import votorola.g.*; 013import votorola.g.hold.*; 014import votorola.g.lang.*; 015 016 017/** A particular revision of the single page at the core of a position. 018 * 019 * @see <a href='http://reluk.ca/w/Category:Position' target='_top'>Category:Position</a> 020 */ 021public interface CoreRevision extends PositionalRevision 022{ 023 024 025 /** Finishes the construction of this CoreRevision and sets {@linkplain 026 * #isFullyConstructed() isFullyConstructed} true. 027 * 028 * @param contextPersonName the name of the person from whose viewpoint the 029 * revision will be viewed (context person), or null if there is no context 030 * person. 031 * @param countSource for possible use during this call. It is never used if 032 * there is no context person. 033 * 034 * @throws IllegalStateException if called a second time. 035 */ 036 public @ThreadRestricted("constructor") void init( PollwikiVS wiki, String contextPersonName, 037 CountSource countSource ) throws IOException; 038 039 040 041 // - C o r e - R e v i s i o n -------------------------------------------------------- 042 043 044 /** Adds to the list the revision path from this core revision (inclusive) to the 045 * terminal {@linkplain #draft() draft} (inclusive). 046 * 047 * @return the same list. 048 */ 049 public List<Integer> addDraftRevisionPath( List<Integer> path ); 050 051 052 053 /** Constructs a view of this core revision suited for viewing in the context of the 054 * specified person, or returns this same (==) core revision if it is already suited. 055 */ 056 public CoreRevision contextView( final String contextPersonName ); 057 058 059 060 /** The position draft. This may be null if the revision is incompletely constructed. 061 */ 062 public DraftRevision draft(); 063 064 065 066 /** Answers whether the construction of this CoreRevision is complete. 067 * 068 * @see #init(PollwikiVS,String,CountSource) 069 */ 070 public boolean isFullyConstructed(); 071 072 073 074 // ==================================================================================== 075 076 077 /** CoreRevision utilities. 078 */ 079 public @ThreadSafe static final class U 080 { 081 082 private U() {} 083 084 085 086 private static final Logger logger = LoggerX.i( CoreRevision.U.class ); 087 088 089 090 /** @param b a string builder comprising 'titles=TITLES' or 'revids=REVS'. 091 */ 092 private static List<Throwable> partialFromQuerySpec( final StringBuilder b, 093 final List<CoreRevision> toCores, 094 final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki, 095 List<Throwable> userXList, final boolean toContinue ) throws IOException 096 { 097 b.insert( 0, "/api.php?format=xml&action=query&" ); 098 b.insert( 0, wiki.scriptURI().toASCIIString() ); 099 b.append( '&' ).append( read_QUERY ); 100 final URL queryURL = new URL( b.toString() ); 101 logger.fine( "querying pollwiki for position cores: " + queryURL ); 102 final Spool spool = new Spool1(); 103 try 104 { 105 final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); 106 for( ;; ) 107 { 108 final CoreRevision core; 109 try{ core = readPartial( xml, wiki, componentPipeClass ); } 110 catch( final MediaWiki.NoSuchItem x ) 111 { 112 if( toContinue ) continue; 113 else throw x; 114 } 115 catch( final IOException x ) 116 { 117 if( userXList == ThrowableX.ENLIST_NONE // == is correct here 118 || !(x instanceof UserInformative )) throw x; 119 120 userXList = ThrowableX.listedThrowable( x, userXList ); 121 continue; 122 } 123 if( core == null ) break; 124 125 toCores.add( core ); 126 } 127 } 128 catch( final XMLStreamException x ) { throw new IOException( x ); } 129 finally{ spool.unwind(); } 130 return userXList; 131 } 132 133 134 135 // -------------------------------------------------------------------------------- 136 137 138 /** Partially constructs the lastest revision of each specified core page and 139 * appends it to the provided list. Constructs LocalDraftRevisions fully and 140 * others partially. Appends revisions in an unspecified order that might not 141 * correspond to the order of elements in pageNameList. 142 * 143 * @param toCores the list to which the partially constructed cores are to be 144 * appended. 145 * @param componentPipeClass the class of component pipe revisions to 146 * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, 147 * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for 148 * neither. 149 * @param userXList a pre-constructed list to which any user actionable 150 * exceptions are to be appended, or null if none was pre-constructed. 151 * Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force 152 * the immediate throwing of such exceptions. 153 * @param toContinue whether to continue in the event of a missing page 154 * (true), or instead to throw a NoSuchPage exception (false). 155 * 156 * @return the same userXList if there was one; otherwise null if no 157 * exceptions were listed; otherwise a newly constructed list. 158 * 159 * @throws MediaWiki.NoSuchPage if toContinue is false and a page is missing. 160 */ 161 public static List<Throwable> partialFromPageNames( final List<? extends CharSequence> pageNameList, 162 final List<CoreRevision> toCores, 163 final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki, 164 final List<Throwable> userXList, final boolean toContinue ) throws IOException 165 { 166 final int pN = pageNameList.size(); 167 if( pN == 0 ) return userXList; // nothing to do 168 169 final StringBuilder b = new StringBuilder(); 170 b.append( "titles=" ); 171 for( int p = 0;; ) // append "name1|name2|name3|" ... 172 { 173 b.append( UriComponent.encode( pageNameList.get(p).toString(), 174 UriComponent.Type.QUERY_PARAM )); 175 ++p; 176 if( p == pN ) break; 177 178 b.append( "%7C" ); // vertical bar (|) 179 } 180 return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList, 181 toContinue ); 182 } 183 184 185 186 /** Partially constructs the specified core revisions and appends them to the 187 * provided list. Constructs LocalDraftRevisions fully and others partially. 188 * Appends revisions in an unspecified order that might not correspond to the 189 * order of elements in revList. 190 * 191 * @param toCores the list to which the partially constructed cores are to be 192 * appended. 193 * @param componentPipeClass the class of component pipe revisions to 194 * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, 195 * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for 196 * neither. 197 * @param userXList a pre-constructed list to which any user actionable 198 * exceptions are to be appended, or null if none was pre-constructed. 199 * Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force 200 * the immediate throwing of such exceptions. 201 * @param toContinue whether to continue in the event of a non-existent 202 * revision (true), or instead to throw a NoSuchRev exception (false). 203 * 204 * @return the same userXList if there was one; otherwise null if no 205 * exceptions were listed; otherwise a newly constructed list. 206 * 207 * @throws MediaWiki.NoSuchRev if toContinue is false and a revision does not 208 * exist. 209 */ 210 public static List<Throwable> partialFromRevs( final List<Integer> revList, 211 final List<CoreRevision> toCores, 212 final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki, 213 final List<Throwable> userXList, final boolean toContinue ) throws IOException 214 { 215 final int rN = revList.size(); 216 if( rN == 0 ) return userXList; // nothing to do 217 218 final StringBuilder b = new StringBuilder(); 219 b.append( "revids=" ); 220 for( int r = 0;; ) // append "rev1|rev2|rev3|" ... 221 { 222 b.append( revList.get(r) ); 223 ++r; 224 if( r == rN ) break; 225 226 b.append( "%7C" ); // vertical bar (|) 227 } 228 return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList, 229 toContinue ); 230 } 231 232 233 234 /** Attempts to fully construct the {@linkplain #contextView(String) context view} 235 * of a CoreRevision from a page query. 236 * 237 * @param xml a reader for the response to a page query in which at least one 238 * 'page' element might remain to be read. The query must cover prop 239 * 'info|revisions' (in order) and rvprop 'content', where the content is 240 * for section 0 only. See for example {@linkplain #read_QUERY 241 * read_QUERY}. 242 * @param componentPipeClass the class of component pipe revisions to 243 * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, 244 * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for 245 * neither. 246 * @param contextPersonName the name of the person from whose viewpoint the 247 * revision will be viewed (context person), or null if there is no context 248 * person. 249 * @param countSource for possible use during this call. It is never used if 250 * there is no context person. 251 * 252 * @return the context view of the fully constructed instance, or null if no 253 * 'page' element remains to be read. 254 * 255 * @throws MediaWiki.NoSuchItem if the page or revision (whichever is 256 * requested) does not exist. 257 */ 258 public static CoreRevision read( final XMLStreamReader xml, final PollwikiVS wiki, 259 final Class<? extends ComponentPipeRevision> componentPipeClass, 260 final String contextPersonName, final CountSource countSource ) 261 throws IOException, XMLStreamException 262 { 263 CoreRevision core = readPartial( xml, wiki, componentPipeClass ); 264 if( core != null ) 265 { 266 core.init( wiki, contextPersonName, countSource ); 267 core = core.contextView( contextPersonName ); 268 } 269 return core; 270 } 271 272 273 274 /** The encoded form of the minimal query parameters required for the {@linkplain 275 * #read(XMLStreamReader,PollwikiVS,Class,String,CountSource) read} and 276 * {@linkplain #readPartial(XMLStreamReader,PollwikiVS,Class) readPartial} 277 * constructors. 278 */ 279 public static final String read_QUERY = 280 "prop=info%7Crevisions&rvprop=content%7Cids&rvsection=0"; 281 282 283 284 /** Attempts to partially construct a CoreRevision from a page query. Yields a 285 * full construction for a LocalDraftRevision, a partial one for any other type, 286 * and nothing at all if the response is exhausted. 287 * 288 * @param xml a reader for the response to a page query in which at least one 289 * 'page' element might remain to be read. The query must cover prop 290 * 'info|revisions' (in order) and rvprop 'content', where the content is 291 * for section 0 only. See for example {@linkplain #read_QUERY 292 * read_QUERY}. 293 * @param componentPipeClass the class of component pipe revisions to 294 * construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1}, 295 * {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for 296 * neither. 297 * 298 * @return the partially constructed instance, or null if no 'page' element 299 * remains to be read. 300 * 301 * @throws MediaWiki.NoSuchItem if the page or revision (whichever is 302 * requested) does not exist. 303 */ 304 public static CoreRevision readPartial( final XMLStreamReader xml, 305 final PollwikiVS wiki, final Class<? extends ComponentPipeRevision> componentPipeClass ) 306 throws IOException, XMLStreamException 307 { 308 // <page> 309 // <categories> 310 // <rev> 311 // </page> 312 while( xml.hasNext() ) 313 { 314 xml.next(); 315 if( !xml.isStartElement() ) continue; 316 317 if( "page".equals( xml.getLocalName() )) 318 { 319 MediaWiki.testPage_missing( xml ); /* when querying by 'titles'; else 320 if querying by 'revids' see 'badrevids' below */ 321 final int revLatest = Integer.parseInt( xml.getAttributeValue( 322 /*ns*/null, "lastrevid" )); 323 final int pageID = Integer.parseInt( xml.getAttributeValue( /*ns*/null, 324 "pageid" )); 325 final String pageName = xml.getAttributeValue( /*ns*/null, "title" ); 326 String content0 = null; 327 int rev = -1; 328 page: while( xml.hasNext() ) 329 { 330 xml.next(); 331 if( xml.isEndElement() && "page".equals( xml.getLocalName() )) break page; 332 // one page, that's all 333 334 if( xml.isStartElement() ) 335 { 336 final String name = xml.getLocalName(); 337 if( "rev".equals( name )) 338 { 339 rev = Integer.parseInt( xml.getAttributeValue( /*ns*/null, 340 "revid" )); 341 content0 = xml.getElementText(); // moves state to </rev> 342 // break page; // last prop requested 343 /// no actual need to be dependent on request order, break on </page> 344 } 345 // else if( "categories".equals( name )) 346 // { 347 // categories: while( xml.hasNext() ) 348 // { 349 // xml.next(); 350 // if( xml.isEndElement() && "categories".equals( xml.getLocalName() )) 351 // { 352 // break categories; 353 // } 354 // 355 // if( xml.isStartElement() && "cl".equals( xml.getLocalName() )) 356 // { 357 // if( "Category:Draft pointer".equals( xml.getAttributeValue( 358 // /*ns*/null, "title" ))) 359 // { 360 // isDraftPointer = true; 361 // break categories; 362 // } 363 // } 364 // } 365 // } 366 /// attempts to optimize pointer detection by restricting pattern 367 /// searches (below) to categorized pages, but fails because category 368 /// is tied to page not rev 369 } 370 } 371 if( content0 == null || rev == -1 ) 372 { 373 throw new MediaWiki.MalformedResponse( "missing 'rev' element" ); 374 } 375 376 if( componentPipeClass != null ) 377 { 378 final MatchResult r = MediaWiki.parsePageNameS( pageName ); 379 if( r == null ) 380 { 381 throw new MediaWiki.MalformedResponse( "malformed page name:" 382 + pageName ); 383 } 384 385 if( /*subpage*/r.group(3) == null ) 386 { 387 final String username = r.group( 2 ); 388 if( wiki.pipeRecognizer().isPipeName( username )) 389 { 390 final Matcher m = 391 ComponentPipeRevision.TEMPLATE_CALL_PATTERN.matcher( content0 ); 392 if( m.find() ) 393 { 394 if( ComponentPipeRevision1.class.equals( componentPipeClass )) 395 { 396 return new ComponentPipeRevision1( wiki, pageID, pageName, 397 rev, revLatest, 398 /*nomineeName*/MediaWiki.normalUsername(m.group(1)), 399 username, /*pollName*/m.group(2) ); 400 } 401 402 if( ComponentPipeRevisionL.class.equals( componentPipeClass )) 403 { 404 return new ComponentPipeRevisionL( wiki, pageID, pageName, 405 rev, revLatest, 406 /*nomineeName*/MediaWiki.normalUsername(m.group(1)), 407 username, /*pollName*/m.group(2) ); 408 } 409 410 throw new IllegalArgumentException( "componentPipeClass" ); 411 } 412 } 413 } 414 } 415 416 // not a component pipe, maybe a draft pointer: 417 final String remotePageName; 418 final String siteName; 419 pointer: 420 { 421 Matcher m; 422 m = PointerRevision.TEMPLATE_CALL_PATTERN_2.matcher( content0 ); 423 if( m.find() ) 424 { 425 remotePageName = m.group( 1 ); 426 siteName = m.group( 2 ); 427 break pointer; 428 } 429 430 m = PointerRevision.TEMPLATE_CALL_PATTERN_1.matcher( content0 ); 431 if( m.find() ) 432 { 433 assert "metagovernment.org".equals( m.group( 1 )); 434 siteName = "Stuff:Metagovernment/wiki"; 435 remotePageName = m.group( 2 ); 436 break pointer; 437 } 438 439 m = PointerRevision.EXTERNAL_LINK_PATTERN.matcher( content0 ); 440 if( m.find() ) 441 { 442 assert "metagovernment.org".equals( m.group( 1 )); 443 siteName = "Stuff:Metagovernment/wiki"; 444 remotePageName = MediaWiki.demiDecodedPageName( UriComponent.decode( 445 m.group(2), UriComponent.Type.QUERY_PARAM )); 446 break pointer; 447 } 448 449 // neither a component pipe nor a draft pointer, so: 450 return new LocalDraftRevision( wiki, pageID, pageName, rev, revLatest, 451 content0 ); 452 } 453 454 final PageProperty pDesign = new PageProperty( "Design" ); 455 final PageProperty pScriptURL = new PageProperty( "Script URL" ); 456 final PageProperty pURL = new PageProperty( "URL" ); 457 final PagePropertyReader in = new PagePropertyReader( wiki.cache(), 458 siteName, pDesign, pScriptURL, pURL ); 459 try 460 { 461 in.readAllRequested(); 462 final String design = pDesign.getValue(); 463 if( !"Stuff:MediaWiki".equals( design )) 464 { 465 throw new PointerRevision.MalformedContent( 466 "unsupported drafting design: " + design, wiki, rev ); 467 } 468 469 final String loc = pURL.getValue(); 470 final String scriptLoc = pScriptURL.getValue(); 471 if( loc == null || scriptLoc == null ) 472 { 473 throw new PointerRevision.MalformedContent( "drafting site '" + siteName 474 + "' missing property '" + pScriptURL.name() + "' or '" + pURL.name() 475 + '\'', wiki, rev ); 476 } 477 478 return new PointerRevision( wiki, pageID, pageName, rev, revLatest, 479 remotePageName, new URI(loc), new URI(scriptLoc), content0 ); 480 } 481 catch( final URISyntaxException x ) 482 { 483 throw new PointerRevision.MalformedURL( x, wiki, rev ); 484 } 485 finally { in.close(); } 486 } 487 MediaWiki.test_badrevids( xml ); /* when querying by 'revids'; else if 488 querying by 'titles' see testPage_missing above */ 489 MediaWiki.test_error( xml ); 490 } 491 return null; 492 } 493 494 495 } 496 497 498}