package 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. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.*; import com.google.gwt.http.client.URL; import com.google.gwt.jsonp.client.JsonpRequestBuilder; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.*; import com.google.web.bindery.event.shared.HandlerRegistration; import votorola.a.count.gwt.*; import votorola.a.diff.*; import votorola.a.web.gwt.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.s.gwt.stage.*; import votorola.s.gwt.stage.vote.*; import static votorola.a.count.CountNode.DART_SECTOR_MAX; import static votorola.s.gwt.stage.vote.LightableDifference.REL_CANDIDATE; import static votorola.s.gwt.stage.vote.LightableDifference.REL_CO; import static votorola.s.gwt.stage.vote.LightableDifference.REL_TIGHT_CYCLE; import static votorola.s.gwt.stage.vote.LightableDifference.REL_VOTER; /** A factory of {@linkplain DifferenceShadows difference shadows} for MediaWiki. */ final class DifferenceShadower implements AsyncCallback, CountCacheListener { /** Creates a DifferenceShadower that enables difference shadowing if the page is a * position draft in correct form, or otherwise does nothing. If shadows are * enabled, then stage property {@linkplain Stage#areDifferencesShadowed() * differencesShadowed} is set true. */ DifferenceShadower() { Stage.i().addInitializer( new TheatreInitializer() // auto-removed { public void initFrom( Stage _s, boolean _rPending ) {} public void initFromComplete( final Stage s, boolean _rPending ) { stageReady( s ); } public void initTo( Stage _s ) {} // rPending public void initTo( final Stage s, final TheatrePage r ) { initR( s, r ); } public void initToComplete( final Stage s, boolean _rPending ) { stageReady( s ); } public void initUltimately( final Stage s, final TheatrePage r ) { initR( s, r ); } private void initR( final Stage s, final TheatrePage r ) // called earlier or later than stageReady(s), if at all { if( shadows == null ) referrer = r; // set later else shadows.setReferrer( r ); // or set now } }); // eventually continues at stageReady } // - A s y n c - C a l l b a c k ------------------------------------------------------ public void onFailure( final Throwable x ) { Stage.i().addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestFail( x, requestLoc )); } public void onSuccess( final JavaScriptObject response ) { final Stage stage = Stage.i(); final JavaScriptObject d = response._get( "d" ); final JsArrayString errors = d._get( "error" ); if( errors != null ) for( int e = 0, eN = errors.length(); e < eN; ++e ) { stage.addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestWarn( errors.get(e), requestLoc )); } ResponseABReader res = new ResponseABReader( d, "A" ); // voters and/or peers final ShadowedDiff[] voterDiffBoard = new ShadowedDiff[DART_SECTOR_MAX]; final ShadowedDiff[] peerDiffBoard = new ShadowedDiff[DART_SECTOR_MAX]; int n = 0; if( res.hasNext() ) { res.next(); n = onSuccess_init( anchorNode.voters(), 0, res, voterDiffBoard, REL_VOTER ); if( res.isEnd() ) n = 0; else // remainder must be peers { assert n == DART_SECTOR_MAX; n = onSuccess_init( peers, 0, res, peerDiffBoard, REL_CO ); assert !res.hasNext(); } } res = new ResponseABReader( d, "B" ); // peers and/or candidate ShadowedDiff candidateDiff = null; // unless found if( res.hasNext() ) { res.next(); n = onSuccess_init( peers, n, res, peerDiffBoard, REL_CO ); candidate: if( !res.isEnd() ) // remainder must be candidate { assert n == DART_SECTOR_MAX; if( candidateNode != null ) { candidateDiff = res.diffOrNull; if( candidateDiff == null ) break candidate; // diff unknown final String candidateDiffKey = res.keyOrNull; assert candidateDiffKey != null; res.verifyMatch( candidateNode.name() ); assert !res.hasNext(); final char rel; if( tightCyclerPosition == 'b' ) // anchor (a) candidate (b) { n = candidateNode.dartSector() - 1; assert voterDiffBoard[n] == null; voterDiffBoard[n] = candidateDiff; // is candidate *and* voter to anchor assert anchorNode.voters().get(n).name().equals( candidateDiff._get( "bUsername" )); rel = REL_TIGHT_CYCLE; } else rel = REL_CANDIDATE; candidateDiff.init( candidateDiffKey, rel, /*sec*/-1, candidateNode, pollName, /*isSelectandB*/false ); } else assert false; } } if( tightCyclerPosition == 'a' ) // candidate (a) anchor (b) { candidateDiff = voterDiffBoard[candidateNode.dartSector()-1]; // is voter *and* candidate to anchor assert candidateNode.name().equals( candidateDiff._get( "aUsername" )); candidateDiff.init2_rel( REL_TIGHT_CYCLE, /*sec*/-1 ); } shadows = new DifferenceShadows( count, anchorNode, voterDiffBoard, peers, peerDiffBoard, candidateNode, candidateDiff, referrer ); stage.setDifferencesShadowed(); NodeV.setDeselectionGuard( "Lax" ); // Allow click to deselect any node in vote track. With default actor set, // 'Default' would be same as 'Lax' but slightly slower. final DifferenceShadowsV shadowsV = new DifferenceShadowsV( shadows, stage ); try { shadowsV.init(); } catch( final DifferenceShadowsV.Warning warning ) { stage.addWarning( warning.toString() ); } new ShadowLight( shadows, shadowsV, stage ); final VoteTrack voteTrack = VoteTrack.i( stage ); // if any is staged if( voteTrack != null ) { final RootPanel body = RootPanel.get(); body.addStyleName( "bottomStaged" ); final FlowPanel stageV = new FlowPanel(); body.add( stageV ); stageV.getElement().setId( "StageV-bottom" ); stageV.addStyleName( "staged" ); stageV.add( new VoteTrackV( voteTrack, /*isBottomFixed*/true )); } } private int onSuccess_init( final JsArray nodeBoard, final int start, final ResponseABReader res, final ShadowedDiff[] diffBoard, final char rel ) { assert !res.isEnd(); int n = start; for(; n < DART_SECTOR_MAX; ++n ) // possible for guard to fail immediately { final CountNodeJS node = nodeBoard.get( n ); if( node == null ) continue; final String nodeName = node.name(); if( nodeName.equals( anchorName )) continue; // only possible in peer board, expect no diff vs. self res.verifyMatch( nodeName ); final ShadowedDiff diff = res.diffOrNull; if( diff != null ) { assert res.keyOrNull != null; diff.init( res.keyOrNull, rel, /*sec*/n+1, node, pollName, /*isSelectandB*/res.isAResponse ); diffBoard[n] = diff; } if( res.hasNext() ) res.next(); else { res.end(); return n + 1; } } return n; } // - C o u n t - C a c h e - L i s t e n e r ------------------------------------------ public void votersSet( final CountNodeEvent e ) { if( hR == null ) return; // initialization is already complete final CountJS count = e.count(); if( !count.pollName().equals( pollName )) return; final CountNodeJS node = e.node(); final String name = node.name(); if( anchorNode == null ) { if( name.equals( anchorName )) { anchorNode = node; DifferenceShadower.this.count = count; if( toShadow() ) shadow(); // else await candidateNode } } else if( name.equals( anchorNode.candidateName() )) { candidateNode = node; shadow(); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private String anchorName; // final after stageReady private CountNodeJS anchorNode; // final after votersSet, complete with voters private CountNodeJS candidateNode; // final after votersSet, complete with voters private CountJS count; // final after votersSet private HandlerRegistration hR; // final after stageReady, nulled if/when shadow private JsArray peers; // final after shadow private String pollName; // final after stageReady private TheatrePage referrer; // if resolved before shadows constructed private String requestLoc; // final after initialized private void shadow() { hR.removeHandler(); hR = null; // Request differences from server. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final StringBuilder a = new StringBuilder(); // a positioned authors AuthorAppender authorAppender; if( candidateNode == null ) { authorAppender = new AuthorAppender( anchorNode.voters() ); if( anchorNode.isCandidate() ) peers = count.baseCandidates(); // anchor is root candidate } else { authorAppender = new AuthorAppender( anchorNode.voters() ) { private final int nTightCycler = candidateNode.isCycler() && anchorName.equals(candidateNode.candidateName())? /*voter index 0-19*/candidateNode.dartSector() - 1: /*none expected*/-1; @Override void append( final StringBuilder sB, final int n, final CountNodeJS node ) { if( n == nTightCycler ) { final String candidateName = candidateNode.name(); tightCyclerPosition = DiffKey.isDartOrdered( anchorNode, anchorName, candidateNode, candidateNode.name() )? 'b':'a'; if( tightCyclerPosition == 'b' ) return; /* tight cycler is both voter and candidate to anchor. This one happens to be ordered like a candidate and is therefore appended directly below */ } super.append( sB, n, node ); } }; peers = candidateNode.voters(); } authorAppender.append( a, 0, DART_SECTOR_MAX ); final StringBuilder b; // b positioned authors final int bN; if( peers == null ) // anchor is orphan { peers = JavaScriptObject.createArray( DART_SECTOR_MAX ).cast(); b = null; bN = 0; } else { b = new StringBuilder(); authorAppender = new AuthorAppender( peers ); final int sectorC = anchorNode.dartSector(); // zero (mosquito) or 1-20 authorAppender.append( a, 0, sectorC - 1 ); authorAppender.append( b, sectorC, DART_SECTOR_MAX ); if( candidateNode != null && tightCyclerPosition != 'a' ) { b.append( candidateNode.name() ); } bN = b.length(); } final int aN = a.length(); if( aN > 0 ) a.deleteCharAt( aN - 1 ); // trailing ( left by init_appendAuthors else if( bN == 0 ) return; // nothing to request final StringBuilder c = GWTX.stringBuilderClear(); c.append( App.getServletContextLocation() ); c.append( "/wap?wCall=dDiff&dPoll=" ).append( URL.encodeQueryString( pollName )); c.append( "&dAnchor=" ).append( URL.encodeQueryString( anchorName )); if( aN > 0 ) c.append( "&dA=" ).append( URL.encodeQueryString( a.toString() )); if( bN > 0 ) c.append( "&dB=" ).append( URL.encodeQueryString( b.toString() )); c.append( "&dPairData&wNonce=" ).append( URLX.serialNonce() ); // per a.web.wap.ResponseConfiguration.headNoCache() requestLoc = c.toString(); App.i().jsonpWAP().requestObject( requestLoc, DifferenceShadower.this ); // continues at onFailure or onSuccess } private DifferenceShadows shadows; // set in onSuccess private void stageReady( final Stage stage ) // after GWTInitCallback methods called and defaults set { // Abort if not position draft in correct form. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - anchorName = stage.getDefaultActorName(); pollName = stage.getDefaultPollName(); if( anchorName == null || pollName == null ) return; // cannot be position draft final JavaScriptObject window = WindowX.js(); final String wgAction = window._get( "wgAction" ); if( !"view".equals(wgAction) && !"purge".equals(wgAction) ) return; // user not viewing text, but editing, looking at history, or something like that final String oldid = Window.Location.getParameter( "oldid" ); if( oldid != null ) { final int rev = Integer.parseInt( oldid ); if( rev != window._getInt( "wgCurRevisionId" )) return; // not the current rev } final Element voDraft = Document.get().getElementById( "voDraft" ); if( voDraft == null ) // not a remote draft, but maybe a local draft { final int wgNamespaceNumber = window._getInt( "wgNamespaceNumber" ); if( wgNamespaceNumber != 2 ) return; // not a user page, cannot be a local draft final JsArrayString categories = window._get( "wgCategories" ); if( categories == null ) return; // maybe impossible for( int c = categories.length() - 1;; --c ) { if( c < 0 ) return; // not categorized as a draft, cannot be a local draft final String category = categories.get( c ); if( "Draft".equals( category )) break; } } // Wait for anchoring count nodes to be fetched and cached. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - assert VoteTrack.i(stage) != null: "stage has node requestor to trigger votersSet"; hR = GWTX.i().bus().addHandlerToSource( CountCacheEvent.TYPE, /*source*/CountCache.i(), DifferenceShadower.this ); // may eventually continue at votersSet } private char tightCyclerPosition; // set 'a' or 'b' if anchor's candidate also anchor's voter private boolean toShadow() { if( anchorNode == null ) return false; if( candidateNode != null ) return true; // got both nodes return !anchorNode.isVoter(); // expect no candidate if no vote cast } // ==================================================================================== private static class AuthorAppender { AuthorAppender( JsArray _nodeBoard ) { nodeBoard = _nodeBoard; } final void append( final StringBuilder b, final int first, final int end/*exclusive*/ ) { for( int n = first; n < end; ++n ) { final CountNodeJS node = nodeBoard.get( n ); if( node != null ) append( b, n, node ); } } void append( final StringBuilder b, final int n, final CountNodeJS node ) { b.append( node.name() ); b.append( '(' ); } private final JsArray nodeBoard; } // ==================================================================================== private static final class ResponseABReader { ResponseABReader( final JavaScriptObject d, final String specName ) { if( "A".equals( specName )) { isAResponse = true; usernamePropertyName = "aUsername"; } else { assert "B".equals( specName ); isAResponse = false; usernamePropertyName = "bUsername"; } diffMap = d._get( "diff" ); diffX = d._get( "diffX" ); keys = diffX._get( specName ); if( keys == null ) kLast = -1; else kLast = keys.length() - 1; } ShadowedDiff diffOrNull; private final JsMap diffMap; private final JavaScriptObject diffX; void end() // go beyond last { k = kLast + 1; diffOrNull = null; } boolean hasNext() { return k < kLast; } final boolean isAResponse; boolean isEnd() { return k > kLast; } // beyond last private int k = -1; String keyOrNull; // null iff diffOrNull is null private final JsArrayString keys; private final int kLast; void next() { assert hasNext(); set( k + 1 ); } private void set( int _k ) { k = _k; keyOrNull = keys.get( k ); if( keyOrNull == null ) diffOrNull = null; else diffOrNull = diffMap.get( keyOrNull ); } private final String usernamePropertyName; void verifyMatch( final String usernameExpected ) { if( diffOrNull == null ) return; /* maybe no diff available, or maybe excluded because of tightCyclerPosition */ final String name = diffOrNull._get( usernamePropertyName ); if( !usernameExpected.equals( name )) { throw new IllegalStateException( "expecting username '" + usernameExpected + "', DiffWAP responded with '" + name + "'" ); } } } }