001package votorola.a.diff; // 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.*; 004import java.io.*; 005import java.net.*; 006import java.util.*; 007import java.util.logging.*; 008import javax.ws.rs.core.*; 009import javax.xml.stream.*; 010import votorola.a.*; 011import votorola.a.count.*; 012import votorola.a.position.*; 013import votorola.a.voter.*; 014import votorola.g.*; 015import votorola.g.hold.*; 016import votorola.g.lang.*; 017import votorola.g.logging.*; 018 019import static votorola.a.position.DraftRevision.MAX_PATH_LENGTH; 020 021 022/** A pair of draft revisions for differencing. 023 */ 024public final @ThreadSafe class DraftPair implements Serializable // where serialized? WP_D.pair 025{ 026 027 028 /** Constructs a DraftPair from a parsed difference key. 029 * 030 * @see #diffKeyParse() 031 */ 032 public static DraftPair newDraftPair( final DiffKeyParse diffKeyParse, final PollwikiVS wiki ) 033 throws IOException 034 { 035 if( diffKeyParse.revisionSeries() != wiki.revisionSeries() ) 036 { 037 throw new RevisionSeriesMismatch( diffKeyParse, wiki ); 038 } 039 final List<Integer> aPath = diffKeyParse.aPath(); 040 final List<Integer> bPath = diffKeyParse.bPath(); 041 if( aPath.equals(bPath) ) 042 { 043 throw new MalformedSpecifier( "identical draft revision paths: " + aPath ); 044 } 045 046 // Construct the core revisions. 047 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 048 final List<Integer> revList = new ArrayList<>( /*initial capacity*/2 ); 049 final CoreRevision aCore; 050 final CoreRevision bCore; 051 { 052 final int aRev = aPath.get( 0 ); 053 final int bRev = bPath.get( 0 ); 054 revList.add( aRev ); 055 revList.add( bRev ); 056 final LinkedList<CoreRevision> cores = new LinkedList<>(); 057 CoreRevision.U.partialFromRevs( revList, cores, ComponentPipeRevision1.class, wiki, 058 ThrowableX.ENLIST_NONE, /*toContinue*/false ); 059 final CoreRevision core1 = cores.getFirst(); 060 final CoreRevision core2 = cores.getLast(); 061 // resolve unpredictable order of response by matching response revs against 062 // those in request: 063 if( core1.rev() == aRev ) 064 { 065 aCore = core1; 066 bCore = core2; 067 } 068 else 069 { 070 aCore = core2; 071 bCore = core1; 072 } 073 } 074 075 // Initialize any component pipes in the cores by constructing their variants. 076 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 077 revList.clear(); // reuse 078 { 079 final List<ComponentPipeRevision1> components = new LinkedList<>(); 080 enlistIfComponentPipe( aCore, components, aPath, /*variant*/revList ); 081 enlistIfComponentPipe( bCore, components, bPath, /*variant*/revList ); 082 final int rN = revList.size(); // 0..2 083 if( rN > 0 ) 084 { 085 // cf. ComponentPipeRevisionL.init2 086 087 final List<CoreRevision> variants = new LinkedList<>(); 088 CoreRevision.U.partialFromRevs( revList, variants, /*componentPipeClass*/null, wiki, 089 ThrowableX.ENLIST_NONE, /*toContinue*/false ); 090 for( final CoreRevision variant: variants ) 091 { 092 final int variantRev = variant.rev(); 093 // response order unpredictable, search for corresponding component(s): 094 for( int r = 0; r < rN; ++r ) 095 { 096 if( revList.get(r) == variantRev ) components.get(r).init( variant ); 097 // keep searching components, other may have same variant 098 } 099 } 100 } 101 } 102 103 // Initialize any pointers among the cores and variants by constructing their remote drafts. 104 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 105 final HashMap<URI,PointerRevList> listMap = new HashMap<>( // key remoteWikiScriptURI 106 /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis 107 enlistAnyPointer( aCore, listMap, aPath ); 108 enlistAnyPointer( bCore, listMap, bPath ); 109 for( final PointerRevList pointerList: listMap.values() ) // each remote wiki 110 { 111 // cf. PointerRevision.initConsume 112 113 final List<Integer> draftRevList = pointerList.draftRevList; 114 final int rN = draftRevList.size(); // 1..2 115 assert rN == pointerList.size(); // one per pointer 116 final URI remoteWikiScriptURI = pointerList.remoteWikiScriptURI; 117 final StringBuilder b = new StringBuilder(); 118 b.append( remoteWikiScriptURI ); 119 b.append( "/api.php?format=xml&action=query&revids=" ); 120 for( int r = 0;; ) // append "rev" or "rev1|rev2" 121 { 122 b.append( draftRevList.get(r) ); 123 ++r; 124 if( r == rN ) break; 125 126 b.append( "%7C" ); // vertical bar (|) 127 } 128 b.append( "&prop=info%7Crevisions&rvprop=ids" ); 129 final URL queryURL = new URL( b.toString() ); 130 logger.fine( "querying remote wiki for drafts: " + queryURL ); 131 final Spool spool = new Spool1(); 132 try 133 { 134 final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); 135 for( ;; ) 136 { 137 final PageRevision remotePage = PageRevision1.readPageRevision( 138 remoteWikiScriptURI, xml, /*toLatest*/false ); 139 if( remotePage == null ) break; 140 141 final int draftRev = remotePage.rev(); 142 // response order unpredictable, search for corresponding pointer(s): 143 for( int r = 0; r < rN; ++r ) 144 { 145 if( draftRevList.get(r) == draftRev ) 146 { 147 final PointerRevision pointer = pointerList.get( r ); 148 pointer.init( new RemoteDraftRevision( pointer, remotePage.pageID(), 149 remotePage.pageName(), remotePage.rev(), remotePage.revLatest() )); 150 } 151 // keep searching pointers, other may point to same remotePage 152 } 153 } 154 } 155 catch( final XMLStreamException x ) { throw new IOException( x ); } 156 finally{ spool.unwind(); } 157 } 158 159 // Construct the draft pair. 160 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 161 return new DraftPair( 162 aCore.contextView(bCore.person().username()), 163 bCore.contextView(aCore.person().username()), diffKeyParse, /*toTestPath*/true ); 164 } 165 166 167 168 /** Constructs a DraftPair from the latest revisions of the specified position cores. 169 * The order of drafts in the resulting pair (aDraft, bDraft) will be lexical by 170 * author name regardless of the order of actual parameters (pageName1, pageName2). 171 * 172 * @param countSource for possible use during this call, or null to construct one 173 * internally. 174 */ 175 public static DraftPair newDraftPair( final String pageName1, final String pageName2, 176 final VoteServer.Run vsRun, CountSource countSource ) throws IOException 177 { 178 if( pageName1.equals( pageName2 )) 179 { 180 throw new MalformedSpecifier( "identical core names: " + pageName1 ); 181 } 182 183 // Construct the core revisions. 184 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 185 final PollwikiVS wiki = vsRun.voteServer().pollwiki(); 186 final CoreRevision aCore; 187 final CoreRevision bCore; 188 { 189 final ArrayList<String> pageNameList = new ArrayList<>( /*initial capacity*/2 ); 190 pageNameList.add( pageName1 ); 191 pageNameList.add( pageName2 ); 192 final LinkedList<CoreRevision> cores = new LinkedList<>(); 193 CoreRevision.U.partialFromPageNames( pageNameList, cores, ComponentPipeRevisionL.class, wiki, 194 ThrowableX.ENLIST_NONE, /*toContinue*/false ); 195 final CoreRevision core1 = cores.getFirst(); 196 final CoreRevision core2 = cores.getLast(); 197 // resolve unpredictable order of response by matching response page names 198 // against those in request: 199 if( DiffKey.isLexicallyOrdered( core1.person().username(), core2.person().username() )) 200 { 201 aCore = core1; 202 bCore = core2; 203 } 204 else 205 { 206 aCore = core2; 207 bCore = core1; 208 } 209 } 210 211 // Initialize any component pipes in the cores by constructing their variants. 212 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 213 { 214 final List<ComponentPipeRevisionL> components = new LinkedList<>(); 215 enlistIfComponentPipe( aCore, components ); 216 enlistIfComponentPipe( bCore, components ); 217 int c = components.size(); 218 if( c > 0 ) 219 { 220 if( countSource == null) countSource = new CountSource1( vsRun ); 221 for( ;; ) // each component 222 { 223 --c; 224 final ComponentPipeRevisionL co = components.get( c ); 225 co.init1( wiki, aCore.person().username(), countSource ); 226 co.init1( wiki, bCore.person().username(), countSource ); 227 if( c == 0 ) break; 228 } 229 ComponentPipeRevisionL.init2( components, wiki, ThrowableX.ENLIST_NONE, 230 /*toContinue*/false ); 231 } 232 } 233 234 // Initialize any pointers among the cores and variants by constructing their remote drafts. 235 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 { 237 final HashMap<URI,PointerList> listMap = new HashMap<>( // key remoteWikiScriptURI 238 /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis 239 enlistAnyPointer( aCore, listMap ); 240 enlistAnyPointer( bCore, listMap ); 241 for( final PointerList pointerList: listMap.values() ) // each remote wiki 242 { 243 PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI, 244 /*toContinue*/false ); 245 } 246 } 247 248 // Construct the draft pair. 249 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 250 return newFromUncontextedViews( aCore, bCore, wiki ); 251 } 252 253 254 255 /** Constructs a DraftPair for the latest of each of the specified position cores that 256 * resolves to a draft (aPageNameList) and appends it to the provided list of pairs. 257 * Uses the latest of bPageName as the second core in each pair. Appends the pairs 258 * in an unspecified order that might not correspond to the order of elements in 259 * aPageNameList. 260 * 261 * @param aPageNameList the name list of first (a) core pages, ideally with spare 262 * capacity to append one additional name. Duplicate names will not result in 263 * duplicate pairs because the MediaWiki API collapses them to one. 264 * @param countSource for possible use during this call, or null to construct one 265 * internally. 266 * @param userXList a pre-constructed list to which any user actionable 267 * exceptions are to be appended, or null if none was pre-constructed. 268 * 269 * @return the same userXList if there was one; otherwise null if no 270 * exceptions were encountered; otherwise a newly constructed list. 271 */ 272 public static List<Throwable> newDraftPairs( final ArrayList<String> aPageNameList, 273 final String bPageName, final VoteServer.Run vsRun, CountSource countSource, 274 final List<DraftPair> pairs, List<Throwable> userXList ) throws IOException 275 { 276 final int aN = aPageNameList.size(); 277 if( aN == 0 ) return userXList; // nothing to do 278 279 // Construct the core revisions. 280 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 281 final PollwikiVS wiki = vsRun.voteServer().pollwiki(); 282 final List<CoreRevision> aCores = new LinkedList<>(); 283 aPageNameList.add( bPageName ); // temporarily 284 try 285 { 286 userXList = CoreRevision.U.partialFromPageNames( aPageNameList, aCores, 287 ComponentPipeRevisionL.class, wiki, userXList, /*toContinue*/true ); 288 } 289 finally{ aPageNameList.remove( aN ); } // restore caller's list 290 if( aCores.size() < 2 ) return userXList; // too many missing, no pair possible 291 292 final CoreRevision bCore; 293 for( final Iterator<CoreRevision> p = aCores.iterator();; ) 294 { 295 final CoreRevision core = p.next(); 296 if( bPageName.equals( core.pageName() )) 297 { 298 p.remove(); 299 bCore = core; 300 break; 301 } 302 303 if( !p.hasNext() ) 304 { 305 return ThrowableX.listedThrowable( new MediaWiki.NoSuchPage( 306 "cannot construct draft pairs against position core \"" + bPageName 307 + "\", pollwiki has no such page", bPageName ), userXList ); 308 } 309 } 310 311 // Initialize any component pipes among the cores by constructing their variants. 312 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 313 { 314 final List<ComponentPipeRevisionL> components = new LinkedList<>(); 315 for( final CoreRevision aCore: aCores ) enlistIfComponentPipe( aCore, components ); 316 enlistIfComponentPipe( bCore, components ); 317 int c = components.size(); 318 if( c > 0 ) 319 { 320 if( countSource == null) countSource = new CountSource1( vsRun ); 321 for( ;; ) // each component 322 { 323 --c; 324 final ComponentPipeRevisionL co = components.get( c ); 325 for( final CoreRevision aCore: aCores ) 326 { 327 co.init1( wiki, aCore.person().username(), countSource ); 328 } 329 co.init1( wiki, bCore.person().username(), countSource ); 330 if( c == 0 ) break; 331 } 332 ComponentPipeRevisionL.init2( components, wiki, userXList, /*toContinue*/true ); 333 } 334 } 335 336 // Initialize any pointers among the cores and variants by constructing their remote drafts. 337 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 338 { 339 final HashMap<URI,PointerList> listMap = new HashMap<>( // key remoteWikiScriptURI 340 /*initial capacity*/(int)((4 + 1) / 0.75f) + 1 ); // per expected max remote wikis 341 for( final CoreRevision aCore: aCores ) enlistAnyPointer( aCore, listMap ); 342 enlistAnyPointer( bCore, listMap ); 343 for( final PointerList pointerList: listMap.values() ) // each remote wiki 344 { 345 PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI, 346 /*toContinue*/true ); 347 } 348 } 349 350 // Construct the draft pairs. 351 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 352 if( bCore.draft() == null ) 353 { 354 return ThrowableX.listedThrowable( new MalformedContent( 355 "cannot construct pairs against bCore, it does not resolve to a draft", bCore ), 356 userXList ); 357 } 358 359 for( final Iterator<CoreRevision> a = aCores.iterator(); a.hasNext(); ) 360 { 361 final CoreRevision aCore = a.next(); 362 if( aCore.isFullyConstructed() ) 363 { 364 pairs.add( newFromUncontextedViews( aCore, bCore, wiki )); 365 } 366 } 367 return userXList; 368 } 369 370 371 372 /** @param _aCore per {@linkplain #aCore() aCore}. It need not already be == 373 * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). 374 * @param _bCore per {@linkplain #bCore() bCore}. It need not already be == 375 * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). 376 */ 377 private static DraftPair newFromUncontextedViews( CoreRevision aCore, CoreRevision bCore, 378 final PollwikiVS wiki ) throws DiffKeyParse.MalformedKey 379 { 380 aCore = aCore.contextView( bCore.person().username() ); 381 bCore = bCore.contextView( aCore.person().username() ); 382 final DiffKeyParse diffKeyParse = new DiffKeyParse( 383 aCore.addDraftRevisionPath( new ArrayList<Integer>(MAX_PATH_LENGTH) ), 384 bCore.addDraftRevisionPath( new ArrayList<Integer>(MAX_PATH_LENGTH) ), 385 wiki.revisionSeries() ); 386 try 387 { 388 return new DraftPair( aCore, bCore, diffKeyParse, /*toTestPath*/false ); 389 } 390 catch( final PathMismatch x ) { throw new VotorolaRuntimeException( x ); } 391 } 392 393 394 395 /** @param _aCore per {@linkplain #aCore() aCore}. It must already be == 396 * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). 397 * @param _bCore per {@linkplain #bCore() bCore}. It must already be == 398 * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). 399 * @param toTestPath whether to test for a PathMismatch. 400 * 401 * @throws IllegalArgumentException if either core's draft is null (incomplete 402 * construction). 403 * @throws PathMismatch if toTestPath is true and either core mismatches. 404 */ 405 private DraftPair( CoreRevision _aCore, CoreRevision _bCore, DiffKeyParse _diffKeyParse, 406 final boolean toTestPath ) throws PathMismatch 407 { 408 init_ensureConstruction( aCore = _aCore ); 409 init_ensureConstruction( bCore = _bCore ); 410 assert aCore == aCore.contextView( bCore.person().username() ); 411 assert bCore == bCore.contextView( aCore.person().username() ); 412 diffKeyParse = _diffKeyParse; 413 if( toTestPath ) 414 { 415 init_testPath( aCore, diffKeyParse.aPath() ); 416 init_testPath( bCore, diffKeyParse.bPath() ); 417 } 418 419 // Construct mnemonics. 420 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 421 final String aUsername = aCore.person().username(); 422 final String bUsername = bCore.person().username(); 423 final StringBuilder aB = IDPair.buildUserMnemonic( aUsername, new StringBuilder() ); 424 final StringBuilder bB = IDPair.buildUserMnemonic( bUsername, new StringBuilder() ); 425 if( aB.toString().equals( bB.toString() )) // identical mnemonics 426 { 427 final int last = aB.length() - 2; 428 int a = last; 429 int b = last; 430 char aChar = aB.charAt( a ); 431 char bChar = aChar; 432 for( ;; ) // find first aChar/bChar that differ 433 { 434 a = init_toNextLetterOrDigit( aUsername, a ); 435 if( a != 0 ) aChar = aUsername.charAt( a ); 436 437 b = init_toNextLetterOrDigit( bUsername, b ); 438 if( b != 0 ) bChar = bUsername.charAt( b ); 439 440 if( aChar != bChar ) 441 { 442 aB.setCharAt( 1, aChar ); 443 bB.setCharAt( 1, bChar ); 444 if( last == 0 ) // we overwrote the trailing '.' 445 { 446 aB.append( '.' ); 447 bB.append( '.' ); 448 } 449 break; 450 } 451 else if( a == 0 && b == 0 ) 452 { 453 aB.setCharAt( 0, Character.toLowerCase( aB.charAt( 0 ))); 454 // lower case of first mnemonic 455 break; 456 } 457 } 458 } 459 aUserMnemonic = aB.toString(); 460 bUserMnemonic = bB.toString(); 461 } 462 463 464 private static void init_ensureConstruction( final CoreRevision core ) 465 { 466 if( !core.isFullyConstructed() ) 467 { 468 throw new IllegalArgumentException( "not fully constructed: " + core ); 469 } 470 471 if( core.draft() == null ) 472 { 473 throw new IllegalArgumentException( new MalformedContent( 474 "cannot construct pairs against core, it does not resolve to a draft", core )); 475 } 476 } 477 478 479 private static void init_testPath( final CoreRevision core, 480 final List<Integer> expectedPath ) throws PathMismatch 481 { 482 final List<Integer> actual = core.addDraftRevisionPath( 483 new ArrayList<Integer>( MAX_PATH_LENGTH )); 484 if( !actual.equals( expectedPath )) throw new PathMismatch( core, actual, expectedPath ); 485 } 486 487 488 /** @param i the current index, or 0 if none. 489 * @return the next index, or 0 if none. 490 */ 491 private static int init_toNextLetterOrDigit( final String username, int i ) 492 { 493 if( i == 0 ) return i; 494 495 for( ;; ) 496 { 497 ++i; 498 if( i >= username.length() ) return 0; 499 500 if( Character.isLetterOrDigit( username.charAt( i ))) return i; 501 } 502 } 503 504 505 506 /** @param _aCore per {@linkplain #aCore() aCore}. It must already be == 507 * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). 508 * @param _bCore per {@linkplain #bCore() bCore}. It must already be == 509 * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). 510 */ 511 private DraftPair( CoreRevision _aCore, String _aUserMnemonic, 512 CoreRevision _bCore, String _bUserMnemonic, DiffKeyParse _diffKeyParse ) 513 { 514 aCore = _aCore; 515 bCore = _bCore; 516 assert aCore == aCore.contextView( bCore.person().username() ); 517 assert bCore == bCore.contextView( aCore.person().username() ); 518 aUserMnemonic = _aUserMnemonic; 519 bUserMnemonic = _bUserMnemonic; 520 diffKeyParse = _diffKeyParse; 521 } 522 523 524 525 // ------------------------------------------------------------------------------------ 526 527 528 /** The core revision of the a-draft. 529 */ 530 public CoreRevision aCore() { return aCore; } 531 532 533 private CoreRevision aCore; 534 535 536 537 /** A short abbreviation of the username of the first draft's author. 538 */ 539 public String aUserMnemonic() { return aUserMnemonic; } 540 541 542 private String aUserMnemonic; 543 544 545 546 /** The core revision of the b-draft. 547 */ 548 public CoreRevision bCore() { return bCore; } 549 550 551 private CoreRevision bCore; 552 553 554 555 /** A short abbreviation of the username of the second draft's author. 556 */ 557 public String bUserMnemonic() { return bUserMnemonic; } 558 559 560 private String bUserMnemonic; 561 562 563 564 /** The difference key of this draft pair. 565 */ 566 public DiffKeyParse diffKeyParse() { return diffKeyParse; } 567 568 569 private final DiffKeyParse diffKeyParse; 570 571 572 573 /** Constructs the reverse of this draft pair, in which all components of a-draft and 574 * b-draft are interchanged. 575 */ 576 public DraftPair newReversePair() 577 { 578 return new DraftPair( bCore, bUserMnemonic, aCore, aUserMnemonic, 579 diffKeyParse.newReverseParse() ); 580 } 581 582 583 584 // ==================================================================================== 585 586 587 /** Thrown when a request cannot be met because the content of a page is not in the 588 * required form. 589 */ 590 static final @ThreadSafe class MalformedContent extends IOException implements UserInformative 591 { 592 MalformedContent( final String message, final PageRevision page ) 593 { 594 super( message + " | " + MediaWiki.revLoc(page.wikiScriptURI(),page.rev()) ); 595 } 596 } 597 598 599 600 // ==================================================================================== 601 602 603 /** Thrown when a request cannot be met because the specifier of a draft pair is not 604 * in the required form. 605 */ 606 static final @ThreadSafe class MalformedSpecifier extends IOException implements UserInformative 607 { 608 MalformedSpecifier( String _message ) { super( _message ); } 609 } 610 611 612 613 // ==================================================================================== 614 615 616 /** Thrown when a request cannot be met because an actual draft revision path is 617 * inconsistent with the expected path. 618 */ 619 static final @ThreadSafe class PathMismatch extends IOException implements UserInformative 620 { 621 622 PathMismatch( final CoreRevision core, final List<Integer> actualPath, 623 final List<Integer> expectedPath ) 624 { 625 super( "the specified draft revision path " + expectedPath 626 + " does not match the actual path " + actualPath + " in the wiki | " 627 + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) ); 628 } 629 630 PathMismatch( final String message, final CoreRevision core ) 631 { 632 super( message + " | " + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) ); 633 } 634 635 } 636 637 638 639 // ==================================================================================== 640 641 642 /** Thrown when a request cannot be met because the revision series of the difference 643 * key no longer matches that of the pollwiki. 644 */ 645 static final @ThreadSafe class RevisionSeriesMismatch extends IOException 646 implements UserInformative 647 { 648 RevisionSeriesMismatch( final DiffKeyParse diffKeyParse, final PollwikiVS wiki ) 649 { 650 super( "cannot resolve the specified difference key " + diffKeyParse 651 + " because the pollwiki revision series has since changed from " 652 + diffKeyParse.revisionSeries() + " to " + wiki.revisionSeries() ); 653 } 654 } 655 656 657 658//// P r i v a t e /////////////////////////////////////////////////////////////////////// 659 660 661 private static void enlistAnyPointer( final CoreRevision core, 662 final Map<URI,PointerList> listMap ) 663 { 664 // (a) ComponentPipeRevisionL version, cf. (b) ComponentPipeRevision1 version 665 666 if( core instanceof ComponentPipeRevisionL ) 667 { 668 final ComponentPipeRevisionL component = (ComponentPipeRevisionL)core; 669 enlistAnyPointer_nonComponent( component.candidateVariant(), listMap ); 670 enlistAnyPointer_nonComponent( component.wildVariant(), listMap ); 671 } 672 else enlistAnyPointer_nonComponent( core, listMap ); 673 } 674 675 676 private static void enlistAnyPointer_nonComponent( CoreRevision coreOrNull, 677 final Map<URI,PointerList> listMap ) 678 { 679 if( coreOrNull instanceof PointerRevision ) 680 { 681 final PointerRevision pointer = (PointerRevision)coreOrNull; 682 final URI uri = pointer.remoteWikiScriptURI(); 683 PointerList pointerList = listMap.get( uri ); 684 if( pointerList == null ) 685 { 686 pointerList = new PointerList( uri ); 687 listMap.put( uri, pointerList ); 688 } 689 pointerList.add( pointer ); 690 } 691 } 692 693 694 695 private static void enlistAnyPointer( CoreRevision core, final Map<URI,PointerRevList> listMap, 696 final List<Integer> revPath ) throws PathMismatch 697 { 698 // (b) ComponentPipeRevision1 version, cf. (a) ComponentPipeRevisionL version 699 700 final int r; 701 if( core instanceof ComponentPipeRevision1 ) 702 { 703 final ComponentPipeRevision1 component = (ComponentPipeRevision1)core; 704 core = component.variant(); // if any 705 r = 2; 706 } 707 else r = 1; 708 if( core instanceof PointerRevision ) 709 { 710 final PointerRevision pointer = (PointerRevision)core; 711 final URI uri = pointer.remoteWikiScriptURI(); 712 PointerRevList pointerList = listMap.get( uri ); 713 if( pointerList == null ) 714 { 715 pointerList = new PointerRevList( uri ); 716 listMap.put( uri, pointerList ); 717 } 718 pointerList.add( pointer ); 719 ensurePath( core, r, revPath ); 720 pointerList.draftRevList.add( revPath.get( r )); 721 } 722 } 723 724 725 726 private static void enlistIfComponentPipe( final CoreRevision core, 727 final List<ComponentPipeRevisionL> components ) 728 { 729 if( core instanceof ComponentPipeRevisionL ) components.add( (ComponentPipeRevisionL)core ); 730 } 731 732 733 734 private static void enlistIfComponentPipe( final CoreRevision core, 735 final List<ComponentPipeRevision1> components, final List<Integer> revPath, 736 final List<Integer> variantRevList ) throws PathMismatch 737 { 738 if( core instanceof ComponentPipeRevision1 ) 739 { 740 components.add( (ComponentPipeRevision1)core ); 741 final int r = 1; 742 ensurePath( core, r, revPath ); 743 variantRevList.add( revPath.get( r )); 744 } 745 } 746 747 748 749 private static void ensurePath( final CoreRevision core, final int r, 750 final List<Integer> revPath ) throws PathMismatch 751 { 752 if( r >= revPath.size() ) 753 { 754 throw new PathMismatch( "must access revision path index " + r 755 + " in order to resolve draft, but the path you specify is too short: " 756 + revPath, core ); 757 } 758 } 759 760 761 762 private static final Logger logger = LoggerX.i( DraftPair.class ); 763 764 765 766 // ==================================================================================== 767 768 769 private static class PointerList extends LinkedList<PointerRevision> 770 { 771 772 PointerList( URI _remoteWikiScriptURI ) { remoteWikiScriptURI = _remoteWikiScriptURI; } 773 774 775 final URI remoteWikiScriptURI; 776 777 } 778 779 780 781 // ==================================================================================== 782 783 784 private static final class PointerRevList extends PointerList 785 { 786 787 PointerRevList( URI _remoteWikiScriptURI ) { super( _remoteWikiScriptURI ); } 788 789 790 final List<Integer> draftRevList = new ArrayList<>( /*initial capacity*/2 ); 791 // one draft rev per pointer 792 793 } 794 795 796}