001package votorola.s.gwt.mediawiki; // Copyright 2012-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.google.gwt.core.client.*; 004import com.google.gwt.dom.client.*; 005import com.google.gwt.http.client.URL; 006import com.google.gwt.jsonp.client.JsonpRequestBuilder; 007import com.google.gwt.user.client.Window; 008import com.google.gwt.user.client.rpc.AsyncCallback; 009import com.google.gwt.user.client.ui.*; 010import com.google.web.bindery.event.shared.HandlerRegistration; 011import votorola.a.count.gwt.*; 012import votorola.a.diff.*; 013import votorola.a.web.gwt.*; 014import votorola.g.lang.*; 015import votorola.g.web.gwt.*; 016import votorola.g.web.gwt.event.*; 017import votorola.s.gwt.stage.*; 018import votorola.s.gwt.stage.vote.*; 019 020import static votorola.a.count.CountNode.DART_SECTOR_MAX; 021import static votorola.s.gwt.stage.vote.LightableDifference.REL_CANDIDATE; 022import static votorola.s.gwt.stage.vote.LightableDifference.REL_CO; 023import static votorola.s.gwt.stage.vote.LightableDifference.REL_TIGHT_CYCLE; 024import static votorola.s.gwt.stage.vote.LightableDifference.REL_VOTER; 025 026 027/** A factory of {@linkplain DifferenceShadows difference shadows} for MediaWiki. 028 */ 029final class DifferenceShadower implements AsyncCallback<JavaScriptObject>, CountCacheListener 030{ 031 032 033 /** Creates a DifferenceShadower that enables difference shadowing if the page is a 034 * position draft in correct form, or otherwise does nothing. If shadows are 035 * enabled, then stage property {@linkplain Stage#areDifferencesShadowed() 036 * differencesShadowed} is set true. 037 */ 038 DifferenceShadower() 039 { 040 Stage.i().addInitializer( new TheatreInitializer() // auto-removed 041 { 042 public void initFrom( Stage _s, boolean _rPending ) {} 043 public void initFromComplete( final Stage s, boolean _rPending ) { stageReady( s ); } 044 public void initTo( Stage _s ) {} // rPending 045 public void initTo( final Stage s, final TheatrePage r ) { initR( s, r ); } 046 public void initToComplete( final Stage s, boolean _rPending ) { stageReady( s ); } 047 public void initUltimately( final Stage s, final TheatrePage r ) { initR( s, r ); } 048 049 private void initR( final Stage s, final TheatrePage r ) 050 // called earlier or later than stageReady(s), if at all 051 { 052 if( shadows == null ) referrer = r; // set later 053 else shadows.setReferrer( r ); // or set now 054 } 055 }); 056 // eventually continues at stageReady 057 } 058 059 060 061 // - A s y n c - C a l l b a c k ------------------------------------------------------ 062 063 064 public void onFailure( final Throwable x ) 065 { 066 Stage.i().addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestFail( x, 067 requestLoc )); 068 } 069 070 071 072 public void onSuccess( final JavaScriptObject response ) 073 { 074 final Stage stage = Stage.i(); 075 final JavaScriptObject d = response._get( "d" ); 076 final JsArrayString errors = d._get( "error" ); 077 if( errors != null ) for( int e = 0, eN = errors.length(); e < eN; ++e ) 078 { 079 stage.addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestWarn( 080 errors.get(e), requestLoc )); 081 } 082 ResponseABReader res = new ResponseABReader( d, "A" ); // voters and/or peers 083 final ShadowedDiff[] voterDiffBoard = new ShadowedDiff[DART_SECTOR_MAX]; 084 final ShadowedDiff[] peerDiffBoard = new ShadowedDiff[DART_SECTOR_MAX]; 085 int n = 0; 086 if( res.hasNext() ) 087 { 088 res.next(); 089 n = onSuccess_init( anchorNode.voters(), 0, res, voterDiffBoard, REL_VOTER ); 090 if( res.isEnd() ) n = 0; 091 else // remainder must be peers 092 { 093 assert n == DART_SECTOR_MAX; 094 n = onSuccess_init( peers, 0, res, peerDiffBoard, REL_CO ); 095 assert !res.hasNext(); 096 } 097 } 098 res = new ResponseABReader( d, "B" ); // peers and/or candidate 099 ShadowedDiff candidateDiff = null; // unless found 100 if( res.hasNext() ) 101 { 102 res.next(); 103 n = onSuccess_init( peers, n, res, peerDiffBoard, REL_CO ); 104 candidate: if( !res.isEnd() ) // remainder must be candidate 105 { 106 assert n == DART_SECTOR_MAX; 107 if( candidateNode != null ) 108 { 109 candidateDiff = res.diffOrNull; 110 if( candidateDiff == null ) break candidate; // diff unknown 111 112 final String candidateDiffKey = res.keyOrNull; assert candidateDiffKey != null; 113 res.verifyMatch( candidateNode.name() ); 114 assert !res.hasNext(); 115 final char rel; 116 if( tightCyclerPosition == 'b' ) // anchor (a) candidate (b) 117 { 118 n = candidateNode.dartSector() - 1; 119 assert voterDiffBoard[n] == null; 120 voterDiffBoard[n] = candidateDiff; // is candidate *and* voter to anchor 121 assert anchorNode.voters().get(n).name().equals( candidateDiff._get( "bUsername" )); 122 rel = REL_TIGHT_CYCLE; 123 } 124 else rel = REL_CANDIDATE; 125 candidateDiff.init( candidateDiffKey, rel, /*sec*/-1, candidateNode, pollName, 126 /*isSelectandB*/false ); 127 } 128 else assert false; 129 } 130 } 131 if( tightCyclerPosition == 'a' ) // candidate (a) anchor (b) 132 { 133 candidateDiff = voterDiffBoard[candidateNode.dartSector()-1]; 134 // is voter *and* candidate to anchor 135 assert candidateNode.name().equals( candidateDiff._get( "aUsername" )); 136 candidateDiff.init2_rel( REL_TIGHT_CYCLE, /*sec*/-1 ); 137 } 138 shadows = new DifferenceShadows( count, anchorNode, voterDiffBoard, peers, peerDiffBoard, 139 candidateNode, candidateDiff, referrer ); 140 stage.setDifferencesShadowed(); 141 NodeV.setDeselectionGuard( "Lax" ); 142 // Allow click to deselect any node in vote track. With default actor set, 143 // 'Default' would be same as 'Lax' but slightly slower. 144 final DifferenceShadowsV shadowsV = new DifferenceShadowsV( shadows, stage ); 145 try { shadowsV.init(); } 146 catch( final DifferenceShadowsV.Warning warning ) { stage.addWarning( warning.toString() ); } 147 new ShadowLight( shadows, shadowsV, stage ); 148 final VoteTrack voteTrack = VoteTrack.i( stage ); // if any is staged 149 if( voteTrack != null ) 150 { 151 final RootPanel body = RootPanel.get(); 152 body.addStyleName( "bottomStaged" ); 153 final FlowPanel stageV = new FlowPanel(); 154 body.add( stageV ); 155 stageV.getElement().setId( "StageV-bottom" ); 156 stageV.addStyleName( "staged" ); 157 stageV.add( new VoteTrackV( voteTrack, /*isBottomFixed*/true )); 158 } 159 } 160 161 162 private int onSuccess_init( final JsArray<CountNodeJS> nodeBoard, final int start, 163 final ResponseABReader res, final ShadowedDiff[] diffBoard, final char rel ) 164 { 165 assert !res.isEnd(); 166 int n = start; 167 for(; n < DART_SECTOR_MAX; ++n ) // possible for guard to fail immediately 168 { 169 final CountNodeJS node = nodeBoard.get( n ); 170 if( node == null ) continue; 171 172 final String nodeName = node.name(); 173 if( nodeName.equals( anchorName )) continue; 174 // only possible in peer board, expect no diff vs. self 175 176 res.verifyMatch( nodeName ); 177 final ShadowedDiff diff = res.diffOrNull; 178 if( diff != null ) 179 { 180 assert res.keyOrNull != null; 181 diff.init( res.keyOrNull, rel, /*sec*/n+1, node, pollName, 182 /*isSelectandB*/res.isAResponse ); 183 diffBoard[n] = diff; 184 } 185 if( res.hasNext() ) res.next(); 186 else 187 { 188 res.end(); 189 return n + 1; 190 } 191 } 192 return n; 193 } 194 195 196 197 // - C o u n t - C a c h e - L i s t e n e r ------------------------------------------ 198 199 200 public void votersSet( final CountNodeEvent e ) 201 { 202 if( hR == null ) return; // initialization is already complete 203 204 final CountJS count = e.count(); 205 if( !count.pollName().equals( pollName )) return; 206 207 final CountNodeJS node = e.node(); 208 final String name = node.name(); 209 if( anchorNode == null ) 210 { 211 if( name.equals( anchorName )) 212 { 213 anchorNode = node; 214 DifferenceShadower.this.count = count; 215 if( toShadow() ) shadow(); // else await candidateNode 216 } 217 } 218 else if( name.equals( anchorNode.candidateName() )) 219 { 220 candidateNode = node; 221 shadow(); 222 } 223 } 224 225 226 227//// P r i v a t e /////////////////////////////////////////////////////////////////////// 228 229 230 231 private String anchorName; // final after stageReady 232 233 234 235 private CountNodeJS anchorNode; // final after votersSet, complete with voters 236 237 238 239 private CountNodeJS candidateNode; // final after votersSet, complete with voters 240 241 242 243 private CountJS count; // final after votersSet 244 245 246 247 private HandlerRegistration hR; // final after stageReady, nulled if/when shadow 248 249 250 251 private JsArray<CountNodeJS> peers; // final after shadow 252 253 254 255 private String pollName; // final after stageReady 256 257 258 259 private TheatrePage referrer; // if resolved before shadows constructed 260 261 262 263 private String requestLoc; // final after initialized 264 265 266 267 private void shadow() 268 { 269 hR.removeHandler(); 270 hR = null; 271 272 // Request differences from server. 273 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 274 final StringBuilder a = new StringBuilder(); // a positioned authors 275 AuthorAppender authorAppender; 276 if( candidateNode == null ) 277 { 278 authorAppender = new AuthorAppender( anchorNode.voters() ); 279 if( anchorNode.isCandidate() ) peers = count.baseCandidates(); 280 // anchor is root candidate 281 } 282 else 283 { 284 authorAppender = new AuthorAppender( anchorNode.voters() ) 285 { 286 private final int nTightCycler = 287 candidateNode.isCycler() && anchorName.equals(candidateNode.candidateName())? 288 /*voter index 0-19*/candidateNode.dartSector() - 1: /*none expected*/-1; 289 @Override void append( final StringBuilder sB, final int n, final CountNodeJS node ) 290 { 291 if( n == nTightCycler ) 292 { 293 final String candidateName = candidateNode.name(); 294 tightCyclerPosition = DiffKey.isDartOrdered( anchorNode, anchorName, 295 candidateNode, candidateNode.name() )? 'b':'a'; 296 if( tightCyclerPosition == 'b' ) return; /* tight cycler is both 297 voter and candidate to anchor. This one happens to be ordered 298 like a candidate and is therefore appended directly below */ 299 } 300 301 super.append( sB, n, node ); 302 } 303 }; 304 peers = candidateNode.voters(); 305 } 306 authorAppender.append( a, 0, DART_SECTOR_MAX ); 307 final StringBuilder b; // b positioned authors 308 final int bN; 309 if( peers == null ) // anchor is orphan 310 { 311 peers = JavaScriptObject.createArray( DART_SECTOR_MAX ).cast(); 312 b = null; 313 bN = 0; 314 } 315 else 316 { 317 b = new StringBuilder(); 318 authorAppender = new AuthorAppender( peers ); 319 final int sectorC = anchorNode.dartSector(); // zero (mosquito) or 1-20 320 authorAppender.append( a, 0, sectorC - 1 ); 321 authorAppender.append( b, sectorC, DART_SECTOR_MAX ); 322 if( candidateNode != null && tightCyclerPosition != 'a' ) 323 { 324 b.append( candidateNode.name() ); 325 } 326 bN = b.length(); 327 } 328 final int aN = a.length(); 329 if( aN > 0 ) a.deleteCharAt( aN - 1 ); // trailing ( left by init_appendAuthors 330 else if( bN == 0 ) return; // nothing to request 331 332 final StringBuilder c = GWTX.stringBuilderClear(); 333 c.append( App.getServletContextLocation() ); 334 c.append( "/wap?wCall=dDiff&dPoll=" ).append( URL.encodeQueryString( pollName )); 335 c.append( "&dAnchor=" ).append( URL.encodeQueryString( anchorName )); 336 if( aN > 0 ) c.append( "&dA=" ).append( URL.encodeQueryString( a.toString() )); 337 if( bN > 0 ) c.append( "&dB=" ).append( URL.encodeQueryString( b.toString() )); 338 c.append( "&dPairData&wNonce=" ).append( URLX.serialNonce() ); 339 // per a.web.wap.ResponseConfiguration.headNoCache() 340 requestLoc = c.toString(); 341 App.i().jsonpWAP().requestObject( requestLoc, DifferenceShadower.this ); 342 // continues at onFailure or onSuccess 343 } 344 345 346 347 private DifferenceShadows shadows; // set in onSuccess 348 349 350 351 private void stageReady( final Stage stage ) // after GWTInitCallback methods called and defaults set 352 { 353 // Abort if not position draft in correct form. 354 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 355 anchorName = stage.getDefaultActorName(); 356 pollName = stage.getDefaultPollName(); 357 if( anchorName == null || pollName == null ) return; // cannot be position draft 358 359 final JavaScriptObject window = WindowX.js(); 360 final String wgAction = window._get( "wgAction" ); 361 if( !"view".equals(wgAction) && !"purge".equals(wgAction) ) return; 362 // user not viewing text, but editing, looking at history, or something like that 363 364 final String oldid = Window.Location.getParameter( "oldid" ); 365 if( oldid != null ) 366 { 367 final int rev = Integer.parseInt( oldid ); 368 if( rev != window._getInt( "wgCurRevisionId" )) return; // not the current rev 369 } 370 371 final Element voDraft = Document.get().getElementById( "voDraft" ); 372 if( voDraft == null ) // not a remote draft, but maybe a local draft 373 { 374 final int wgNamespaceNumber = window._getInt( "wgNamespaceNumber" ); 375 if( wgNamespaceNumber != 2 ) return; // not a user page, cannot be a local draft 376 377 final JsArrayString categories = window._get( "wgCategories" ); 378 if( categories == null ) return; // maybe impossible 379 380 for( int c = categories.length() - 1;; --c ) 381 { 382 if( c < 0 ) return; // not categorized as a draft, cannot be a local draft 383 384 final String category = categories.get( c ); 385 if( "Draft".equals( category )) break; 386 } 387 } 388 389 // Wait for anchoring count nodes to be fetched and cached. 390 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 391 assert VoteTrack.i(stage) != null: "stage has node requestor to trigger votersSet"; 392 hR = GWTX.i().bus().addHandlerToSource( CountCacheEvent.TYPE, /*source*/CountCache.i(), 393 DifferenceShadower.this ); 394 // may eventually continue at votersSet 395 } 396 397 398 399 private char tightCyclerPosition; // set 'a' or 'b' if anchor's candidate also anchor's voter 400 401 402 403 private boolean toShadow() 404 { 405 if( anchorNode == null ) return false; 406 407 if( candidateNode != null ) return true; // got both nodes 408 409 return !anchorNode.isVoter(); // expect no candidate if no vote cast 410 } 411 412 413 414 // ==================================================================================== 415 416 417 private static class AuthorAppender 418 { 419 420 AuthorAppender( JsArray<CountNodeJS> _nodeBoard ) { nodeBoard = _nodeBoard; } 421 422 423 final void append( final StringBuilder b, final int first, final int end/*exclusive*/ ) 424 { 425 for( int n = first; n < end; ++n ) 426 { 427 final CountNodeJS node = nodeBoard.get( n ); 428 if( node != null ) append( b, n, node ); 429 } 430 } 431 432 433 void append( final StringBuilder b, final int n, final CountNodeJS node ) 434 { 435 b.append( node.name() ); 436 b.append( '(' ); 437 } 438 439 440 private final JsArray<CountNodeJS> nodeBoard; 441 442 } 443 444 445 446 // ==================================================================================== 447 448 449 private static final class ResponseABReader 450 { 451 452 ResponseABReader( final JavaScriptObject d, final String specName ) 453 { 454 if( "A".equals( specName )) 455 { 456 isAResponse = true; 457 usernamePropertyName = "aUsername"; 458 } 459 else 460 { 461 assert "B".equals( specName ); 462 isAResponse = false; 463 usernamePropertyName = "bUsername"; 464 } 465 diffMap = d._get( "diff" ); 466 diffX = d._get( "diffX" ); 467 keys = diffX._get( specName ); 468 if( keys == null ) kLast = -1; 469 else kLast = keys.length() - 1; 470 } 471 472 473 ShadowedDiff diffOrNull; 474 475 private final JsMap<ShadowedDiff> diffMap; 476 477 private final JavaScriptObject diffX; 478 479 480 void end() // go beyond last 481 { 482 k = kLast + 1; 483 diffOrNull = null; 484 } 485 486 487 boolean hasNext() { return k < kLast; } 488 489 490 final boolean isAResponse; 491 492 493 boolean isEnd() { return k > kLast; } // beyond last 494 495 496 private int k = -1; 497 498 String keyOrNull; // null iff diffOrNull is null 499 500 private final JsArrayString keys; 501 502 private final int kLast; 503 504 505 void next() 506 { 507 assert hasNext(); 508 set( k + 1 ); 509 } 510 511 512 private void set( int _k ) 513 { 514 k = _k; 515 keyOrNull = keys.get( k ); 516 if( keyOrNull == null ) diffOrNull = null; 517 else diffOrNull = diffMap.get( keyOrNull ); 518 } 519 520 521 private final String usernamePropertyName; 522 523 524 void verifyMatch( final String usernameExpected ) 525 { 526 if( diffOrNull == null ) return; /* maybe no diff available, or maybe excluded 527 because of tightCyclerPosition */ 528 529 final String name = diffOrNull._get( usernamePropertyName ); 530 if( !usernameExpected.equals( name )) 531 { 532 throw new IllegalStateException( "expecting username '" + usernameExpected 533 + "', DiffWAP responded with '" + name + "'" ); 534 } 535 } 536 537 } 538 539 540}