001package votorola.s.gwt.stage.link; // 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.user.client.rpc.AsyncCallback;
007import com.google.web.bindery.event.shared.HandlerRegistration;
008import java.util.*;
009import java.util.regex.*;
010import votorola.a.count.gwt.*;
011import votorola.a.diff.*;
012import votorola.a.web.gwt.*;
013import votorola.g.*;
014import votorola.g.web.gwt.*;
015import votorola.g.web.gwt.event.*;
016import votorola.s.gwt.stage.*;
017import votorola.s.gwt.stage.vote.*;
018
019import static votorola.a.count.CountNode.DART_SECTOR_MAX;
020import static votorola.s.gwt.stage.vote.DifferenceLight.MAX_DIFFS;
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 targeter for a difference link with support for nominal targeting.  Nominal
028  * targeting specifies differences in terms of poll and author names (aAuthor and bAuthor
029  * in {@linkplain votorola.s.wic.diff.WP_D WP_D}) as opposed to the revision numbers of
030  * draft pages.  A nominal target always refers to the latest difference of the two
031  * authors.  This particular targeter is restricted to differences between the
032  * {@linkplain VoteTrack#anchor() anchor} and his/her direct {@linkplain
033  * votorola.a.count.XCastRelation cast relations}.  It nominally targets the link
034  * whenever the following conditions are met:<ul>
035  *
036  * <li>The link would not otherwise be targeted to an actual page location, either
037  * because there is no {@linkplain Stage#getDifference staged difference}, the staged
038  * difference is already nominal, or it is already shown in the present page (as for
039  * instance in the {@linkplain votorola.s.wic.diff.WP_D bridge}).</li>
040  *
041  * <li>A {@linkplain Stage#getPollName poll is staged}.</li>
042  *
043  * <li>An {@linkplain Stage#getActorName actor is staged} who is a direct cast relation
044  * within that poll of the anchor.</li>
045  *
046  * <li>Both anchor and actor have formal positions in the poll and so assumedly <a
047  * href='http://reluk.ca/w/Category:Draft' target='_top'>drafts</a>.</li></ul>
048  *
049  * <p>Under these conditions, the link is nominally targeted and the stage is set to
050  * {@linkplain #NOMINAL_DIFF NOMINAL_DIFF}.</p>
051  */
052public final class NominalDifferenceTargeter extends LinkTrackV.TargeterN
053  implements AsyncCallback<JavaScriptObject>, ChangeHandler
054{
055
056
057    /** Does nothing itself but the call forces static initialization of this class.
058      */
059    public static void forceInitClass() {}
060
061
062
063    /** Creates a NominalDifferenceTargeter.  Create at most one for the entire life of the
064      * document, as currently it does not unregister its listeners or otherwise clean up
065      * after itself.
066      */
067    NominalDifferenceTargeter( final LinkTrackV trackV, final AnchorElement diffLink )
068    {
069        trackV.super( diffLink );
070        final Stage stage = Stage.i();
071        voteTrack = VoteTrack.i( stage );
072        if( voteTrack == null ) throw new IllegalStateException( "no vote track" );
073
074        new NominalDifferenceLight( stage, NominalDifferenceTargeter.this );
075        GWTX.i().bus().addHandlerToSource( Change.TYPE, /*source*/voteTrack,
076          NominalDifferenceTargeter.this ); // no need to unregister, registry does not outlive this listener
077    }
078
079
080
081    private static NominalDifferenceTargeter instance;
082
083        {
084            if( instance != null ) throw new IllegalStateException();
085              // need multiple instances?  factor out diffMap and its expensive populating
086              // code as a singleton for use by the NominalDifferenceLight singleton
087
088            instance = NominalDifferenceTargeter.this;
089        }
090
091
092
093   // ------------------------------------------------------------------------------------
094
095
096    /** The map of lightable differences keyed by author name.  Changes to map state are
097      * signalled by {@linkplain Change change events} on the {@linkplain GWTX#bus() bus}.
098      * The event source is {@linkplain #diffMapS diffMapS}.
099      */
100    Map<String,LightableDifference1> diffMap() { return diffMap; }
101
102
103        private final Map<String,LightableDifference1> diffMap =  // all but the anchor
104          new HashMap<String,LightableDifference1>(
105            /*init capacity*/(int)((MAX_DIFFS + 1) / 0.75f) + 1 );
106
107
108        private void diffMap_clear()
109        {
110            final boolean toFire = diffMap.size() > 0;
111            diffMap.clear();
112            if( toFire ) diffMap_fire();
113        }
114
115
116        private void diffMap_fire() { GWTX.i().bus().fireEventFromSource( new Change(), diffMapS ); }
117
118
119        /** The source of change events for diffMap.
120          */
121        final Object diffMapS = new Object(); // workaround, grep CAES
122
123
124
125    /** Answers whether nominal targeting is enabled.
126      */
127    public static boolean isEnabled() { return isEnabled; }
128
129
130        private static boolean isEnabled;
131
132
133        /** Enables nominal targeting.  Call this method from the global configuration
134          * function {@linkplain StageMod voGWTConfig.s_gwt_stage} in this fashion:<pre
135          *
136         *>   s_gwt_stage_link_NominalDifferenceTargeter_setEnabled();</pre>
137          */
138        public static @GWTConfigCallback void setEnabled() { isEnabled = true; }
139
140
141        private static native void exposeEnabled()
142        /*-{
143            $wnd.s_gwt_stage_link_NominalDifferenceTargeter_setEnabled = $entry(
144              @votorola.s.gwt.stage.link.NominalDifferenceTargeter::setEnabled() );
145        }-*/;
146
147
148        static
149        {
150            assert StageMod.isForcedInit(): "forced init " + NominalDifferenceTargeter.class.getName();
151            exposeEnabled();
152        }
153
154
155
156    /** The difference that is {@linkplain Stage#getDifference() staged} whenever the
157      * difference link is nominally targeted.  The diff key is fabricated and very
158      * unlikely to correspond to an actual difference in the wiki.
159      */
160    public static final DiffLook NOMINAL_DIFF;
161
162        static
163        {
164            final String rS = Integer.toString( Integer.MAX_VALUE ); // unlikely rev
165            final String veryUnlikelyKey = rS + '.' + rS + '.' + rS + '-' + rS + '.' + rS + '.' + rS;
166            NOMINAL_DIFF = new DiffLook1( veryUnlikelyKey, /*selectand*/"a",
167              /*toPersist*/false/*because weird and actor names effectively persist it anyway*/ );
168        }
169
170
171
172    @Override void retarget()
173    {
174        final Stage stage = Stage.i();
175        final DiffLook look = stage.getDifference();
176        final boolean wasNominal = look == NOMINAL_DIFF;
177        boolean isNominal = false; // thus far
178        String href = null;
179        if( look != null && !wasNominal ) href = DifferenceTargeter.href( look );
180          // assigns null if that page already showing
181        if( href == null ) // either no diff is staged, or its page is already showing
182        {
183            final String actorName = stage.getActorName();
184            if( actorName != null )
185            {
186                final CountNodeJS anchor = voteTrack.anchor();
187                if( anchor != null )
188                {
189                    final String anchorName = anchor.name();
190                    if( !anchorName.equals( actorName )) // then pair of authors is staged
191                    {
192                        final LightableDifference1 diff = diffMap.get( actorName );
193                        if( diff != null )
194                        {
195                            isNominal = true;
196                            final boolean isSelectandA = "a".equals( diff.selectand() );
197                            final String aAuthor;
198                            final String bAuthor; // correct order will prevent 2nd redirection
199                            if( isSelectandA )
200                            {
201                                aAuthor = anchorName;
202                                bAuthor = actorName;
203                            }
204                            else
205                            {
206                                aAuthor = actorName;
207                                bAuthor = anchorName;
208                            }
209                            final StringBuilder b = GWTX.stringBuilderClear();
210                            b.append( App.getServletContextLocation() ).append( "/w/D?" );
211                            b.append(  "aAuthor=" ).append( URL.encodeQueryString( aAuthor ));
212                            b.append( "&bAuthor=" ).append( URL.encodeQueryString( bAuthor ));
213                            b.append( "&poll=" ).append( URL.encodeQueryString(
214                              voteTrack.count().pollName() ));
215                            if( isSelectandA ) b.append( "&s" ); // reversing it, so other selected
216                            href = b.toString();
217                        }
218                    }
219                }
220            }
221        }
222        setTarget( adjustedTarget( href ));
223        if( isNominal )
224        {
225            if( !wasNominal ) stage.setDifference( NOMINAL_DIFF );
226        }
227        else if( wasNominal ) stage.setDifference( null );
228    }
229
230
231
232    /** The staged instance of the vote track.
233      */
234    VoteTrack voteTrack() { return voteTrack; }
235
236
237        private final VoteTrack voteTrack;
238
239
240
241   // - A s y n c - C a l l b a c k ------------------------------------------------------
242
243
244    public void onFailure( final Throwable x )
245    {
246        Stage.i().addWarning( App.i().mesS().gwt_stage_link_NominalDifferenceTargeter_requestFail( x,
247          onChange_requestLoc ));
248    }
249
250
251
252    public void onSuccess( final JavaScriptObject response )
253    {
254        diffMap_clear(); // in case of delayed response, at least clear previous response's authors
255        try
256        {
257            final Board peers = voteTrack.peers();
258            final Board voters = voteTrack.voters();
259            if( !isVoteTrackReady( peers, voters )) return; // abort response, it's changing again
260
261            JavaScriptObjectX pages = response._get( "query" );
262            if( pages != null ) pages = pages._get( "pages" );
263            if( pages == null )
264            {
265                Stage.i().addWarning( App.i().mesS().gwt_stage_link_NominalDifferenceTargeter_requestWarn(
266                  "response has no 'pages'", onChange_requestLoc ));
267                return;
268            }
269
270            final CountNodeJS anchor = voteTrack.anchor();
271            if( anchor == null ) throw new IllegalStateException( "isVoteTrackReady and no anchor" );
272
273            final String anchorName = anchor.name();
274            boolean isAnchorAuthor = false; // till proven otherwise
275            for( String p: pages._in() )
276            {
277                if( p.charAt(0) == '_' ) continue; // skip __gwt_ObjectId, seen on Chrome
278
279                final JavaScriptObject page = pages._get( p );
280                if( !page._hasProperty( "pageid" )) continue; // no position page
281
282                final String pageName = page._getString( "title" );
283                final MatchResult m = MediaWiki.parsePageNameS( pageName );
284                if( m == null ) throw new IllegalStateException( "malformed page name" );
285
286                final String authorName = m.group( 2 ); // in canonical form already, API does that
287                if( authorName.equals( anchorName ))
288                {
289                    isAnchorAuthor = true;
290                    continue;
291                }
292
293                final char rel;
294                final int dartSector;
295                final String selectand;
296                final CountNodeJS candidate = voteTrack.candidate();
297                if( candidate != null && candidate.name().equals( authorName ))
298                {
299                    if( voteTrack.hasTightCycle() )
300                    {
301                        rel = REL_TIGHT_CYCLE;
302                        selectand = DiffKey.isDartOrdered( anchor, anchorName, candidate,
303                          candidate.name() )? "a":"b";
304                    }
305                    else
306                    {
307                        rel = REL_CANDIDATE;
308                        selectand = "a";
309                    }
310                    dartSector = -1; // not used
311                }
312                else
313                {
314                    CountNodeJS node;
315                    node = onSuccess_find( authorName, voteTrack.voters() );
316                    if( node != null )
317                    {
318                        rel = REL_VOTER;
319                        selectand = "b";
320                    }
321                    else
322                    {
323                        node = onSuccess_find( authorName, voteTrack.peers() );
324                        if( node != null )
325                        {
326                            rel = REL_CO;
327                            selectand = DiffKey.isDartOrdered( anchor, anchorName, node,
328                              node.name() )? "a":"b";
329                        }
330                        else // very unlikely
331                        {
332                            Stage.i().addWarning( App.i().mesS()
333                                .gwt_stage_link_NominalDifferenceTargeter_requestWarn(
334                             "author no longer in track, wiki responses maybe out of sequence",
335                              onChange_requestLoc ));
336                            continue; // hope for the best
337                        }
338                    }
339
340                    dartSector = node.dartSector();
341                }
342                diffMap.put( authorName, new LightableDifference1( rel, dartSector, selectand,
343                  authorName, voteTrack.count().pollName() ));
344            }
345            if( !isAnchorAuthor ) diffMap.clear(); /* undo because there can be no
346              differences with anchor, after all.  No need to fire events or anything,
347              this is a plain undo */
348            else if( diffMap.size() > 0 ) diffMap_fire(); // because it changed
349        }
350        finally{ retarget(); }
351    }
352
353
354        private CountNodeJS onSuccess_find( final String authorName, final Board board )
355        {
356            CountNodeJS node;
357            node = board.mosquito();
358            if( node != null && node.name().equals( authorName )) return node;
359
360            for( int s = 1; s <= DART_SECTOR_MAX; ++s )
361            {
362                node = board.node( s );
363                if( node != null && node.name().equals( authorName )) return node;
364            }
365
366            return null;
367        }
368
369
370
371   // - C h a n g e - H a n d l e r - ----------------------------------------------------
372
373
374    public void onChange( Change _e )
375    {
376        final Board peers = voteTrack.peers();
377        final Board voters = voteTrack.voters();
378        if( !isVoteTrackReady( peers, voters )) return; // wait for it
379
380        final String pollName = voteTrack.count().pollName();
381        final StringBuilder b = GWTX.stringBuilderClear();
382        final PollwikiG wiki = App.i().pollwiki();
383        onChange_append( b, voters, pollName, wiki );
384        onChange_append( b, voteTrack.peers(), pollName, wiki );
385        final CountNodeJS candidate = voteTrack.candidate();
386        if( candidate != null )
387        {
388            if( voteTrack.hasTightCycle() ) assert b.length() > 0; // already appended as voter
389            else onChange_append( b, candidate, pollName, wiki );
390        }
391        else if( b.length() == 0 ) return; // anchor is orphan
392          // [orphan?  length zero seems impossible for an orphan, which would be appended
393          // as mosquito in peer board]
394
395        b.deleteCharAt( b.length() - 1 ); // remove trailing separator '|'
396        b.insert( 0, "/api.php?titles=" ); // limit 50 titles, need 21 * 2 + 1 = 43 max
397        b.insert( 0, PollwikiG.getScriptLocation() );
398        b.append( "&action=query&format=json" );
399        onChange_requestLoc = b.toString();
400        App.i().jsonp().requestObject( onChange_requestLoc, NominalDifferenceTargeter.this );
401        // continues at onFailure or onSuccess
402    }
403
404
405        private static void onChange_append( final StringBuilder b, final Board board,
406          final String pollName, final PollwikiG wiki )
407        {
408            CountNodeJS node;
409            node = board.mosquito();
410            if( node != null ) onChange_append( b, node, pollName, wiki );
411            for( int s = 1; s <= DART_SECTOR_MAX; ++s )
412            {
413                node = board.node( s );
414                if( node != null ) onChange_append( b, node, pollName, wiki );
415            }
416        }
417
418
419        private static void onChange_append( final StringBuilder b, final CountNodeJS node,
420          final String pollName, final PollwikiG wiki )
421        {
422            b.append( URL.encodeQueryString( wiki.positionPageName( node.name(), pollName )));
423            b.append( '|' );
424        }
425
426
427        private String onChange_requestLoc;
428
429
430
431   // - P r o p e r t y - C h a n g e - H a n d l e r ------------------------------------
432
433
434    public void onPropertyChange( final PropertyChange e )
435    {
436        final String pName = e.propertyName();
437        if( pName.equals("actorName") || pName.equals("difference") ) coalescer.schedule();
438        else if( pName.equals( "pollName" ))
439        {
440            diffMap_clear(); // till sync with vote track completes
441            coalescer.schedule();
442        }
443        // eventually continues at retarget if coalescer called
444    }
445
446
447
448//// P r i v a t e ///////////////////////////////////////////////////////////////////////
449
450
451    private static boolean isVoteTrackReady( final Board peers, final Board voters )
452    {
453        return voters.isEnabled()/*anchored*/ && peers.isEnabled()/*full track*/;
454    }
455
456
457}