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}