package 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. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.*; import com.google.gwt.http.client.URL; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.web.bindery.event.shared.HandlerRegistration; import java.util.*; import java.util.regex.*; import votorola.a.count.gwt.*; import votorola.a.diff.*; import votorola.a.web.gwt.*; import votorola.g.*; 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.DifferenceLight.MAX_DIFFS; 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 targeter for a difference link with support for nominal targeting. Nominal * targeting specifies differences in terms of poll and author names (aAuthor and bAuthor * in {@linkplain votorola.s.wic.diff.WP_D WP_D}) as opposed to the revision numbers of * draft pages. A nominal target always refers to the latest difference of the two * authors. This particular targeter is restricted to differences between the * {@linkplain VoteTrack#anchor() anchor} and his/her direct {@linkplain * votorola.a.count.XCastRelation cast relations}. It nominally targets the link * whenever the following conditions are met: * *

Under these conditions, the link is nominally targeted and the stage is set to * {@linkplain #NOMINAL_DIFF NOMINAL_DIFF}.

*/ public final class NominalDifferenceTargeter extends LinkTrackV.TargeterN implements AsyncCallback, ChangeHandler { /** Does nothing itself but the call forces static initialization of this class. */ public static void forceInitClass() {} /** Creates a NominalDifferenceTargeter. Create at most one for the entire life of the * document, as currently it does not unregister its listeners or otherwise clean up * after itself. */ NominalDifferenceTargeter( final LinkTrackV trackV, final AnchorElement diffLink ) { trackV.super( diffLink ); final Stage stage = Stage.i(); voteTrack = VoteTrack.i( stage ); if( voteTrack == null ) throw new IllegalStateException( "no vote track" ); new NominalDifferenceLight( stage, NominalDifferenceTargeter.this ); GWTX.i().bus().addHandlerToSource( Change.TYPE, /*source*/voteTrack, NominalDifferenceTargeter.this ); // no need to unregister, registry does not outlive this listener } private static NominalDifferenceTargeter instance; { if( instance != null ) throw new IllegalStateException(); // need multiple instances? factor out diffMap and its expensive populating // code as a singleton for use by the NominalDifferenceLight singleton instance = NominalDifferenceTargeter.this; } // ------------------------------------------------------------------------------------ /** The map of lightable differences keyed by author name. Changes to map state are * signalled by {@linkplain Change change events} on the {@linkplain GWTX#bus() bus}. * The event source is {@linkplain #diffMapS diffMapS}. */ Map diffMap() { return diffMap; } private final Map diffMap = // all but the anchor new HashMap( /*init capacity*/(int)((MAX_DIFFS + 1) / 0.75f) + 1 ); private void diffMap_clear() { final boolean toFire = diffMap.size() > 0; diffMap.clear(); if( toFire ) diffMap_fire(); } private void diffMap_fire() { GWTX.i().bus().fireEventFromSource( new Change(), diffMapS ); } /** The source of change events for diffMap. */ final Object diffMapS = new Object(); // workaround, grep CAES /** Answers whether nominal targeting is enabled. */ public static boolean isEnabled() { return isEnabled; } private static boolean isEnabled; /** Enables nominal targeting. Call this method from the global configuration * function {@linkplain StageMod voGWTConfig.s_gwt_stage} in this fashion:
   s_gwt_stage_link_NominalDifferenceTargeter_setEnabled();
*/ public static @GWTConfigCallback void setEnabled() { isEnabled = true; } private static native void exposeEnabled() /*-{ $wnd.s_gwt_stage_link_NominalDifferenceTargeter_setEnabled = $entry( @votorola.s.gwt.stage.link.NominalDifferenceTargeter::setEnabled() ); }-*/; static { assert StageMod.isForcedInit(): "forced init " + NominalDifferenceTargeter.class.getName(); exposeEnabled(); } /** The difference that is {@linkplain Stage#getDifference() staged} whenever the * difference link is nominally targeted. The diff key is fabricated and very * unlikely to correspond to an actual difference in the wiki. */ public static final DiffLook NOMINAL_DIFF; static { final String rS = Integer.toString( Integer.MAX_VALUE ); // unlikely rev final String veryUnlikelyKey = rS + '.' + rS + '.' + rS + '-' + rS + '.' + rS + '.' + rS; NOMINAL_DIFF = new DiffLook1( veryUnlikelyKey, /*selectand*/"a", /*toPersist*/false/*because weird and actor names effectively persist it anyway*/ ); } @Override void retarget() { final Stage stage = Stage.i(); final DiffLook look = stage.getDifference(); final boolean wasNominal = look == NOMINAL_DIFF; boolean isNominal = false; // thus far String href = null; if( look != null && !wasNominal ) href = DifferenceTargeter.href( look ); // assigns null if that page already showing if( href == null ) // either no diff is staged, or its page is already showing { final String actorName = stage.getActorName(); if( actorName != null ) { final CountNodeJS anchor = voteTrack.anchor(); if( anchor != null ) { final String anchorName = anchor.name(); if( !anchorName.equals( actorName )) // then pair of authors is staged { final LightableDifference1 diff = diffMap.get( actorName ); if( diff != null ) { isNominal = true; final boolean isSelectandA = "a".equals( diff.selectand() ); final String aAuthor; final String bAuthor; // correct order will prevent 2nd redirection if( isSelectandA ) { aAuthor = anchorName; bAuthor = actorName; } else { aAuthor = actorName; bAuthor = anchorName; } final StringBuilder b = GWTX.stringBuilderClear(); b.append( App.getServletContextLocation() ).append( "/w/D?" ); b.append( "aAuthor=" ).append( URL.encodeQueryString( aAuthor )); b.append( "&bAuthor=" ).append( URL.encodeQueryString( bAuthor )); b.append( "&poll=" ).append( URL.encodeQueryString( voteTrack.count().pollName() )); if( isSelectandA ) b.append( "&s" ); // reversing it, so other selected href = b.toString(); } } } } } setTarget( adjustedTarget( href )); if( isNominal ) { if( !wasNominal ) stage.setDifference( NOMINAL_DIFF ); } else if( wasNominal ) stage.setDifference( null ); } /** The staged instance of the vote track. */ VoteTrack voteTrack() { return voteTrack; } private final VoteTrack voteTrack; // - 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_stage_link_NominalDifferenceTargeter_requestFail( x, onChange_requestLoc )); } public void onSuccess( final JavaScriptObject response ) { diffMap_clear(); // in case of delayed response, at least clear previous response's authors try { final Board peers = voteTrack.peers(); final Board voters = voteTrack.voters(); if( !isVoteTrackReady( peers, voters )) return; // abort response, it's changing again JavaScriptObjectX pages = response._get( "query" ); if( pages != null ) pages = pages._get( "pages" ); if( pages == null ) { Stage.i().addWarning( App.i().mesS().gwt_stage_link_NominalDifferenceTargeter_requestWarn( "response has no 'pages'", onChange_requestLoc )); return; } final CountNodeJS anchor = voteTrack.anchor(); if( anchor == null ) throw new IllegalStateException( "isVoteTrackReady and no anchor" ); final String anchorName = anchor.name(); boolean isAnchorAuthor = false; // till proven otherwise for( String p: pages._in() ) { if( p.charAt(0) == '_' ) continue; // skip __gwt_ObjectId, seen on Chrome final JavaScriptObject page = pages._get( p ); if( !page._hasProperty( "pageid" )) continue; // no position page final String pageName = page._getString( "title" ); final MatchResult m = MediaWiki.parsePageNameS( pageName ); if( m == null ) throw new IllegalStateException( "malformed page name" ); final String authorName = m.group( 2 ); // in canonical form already, API does that if( authorName.equals( anchorName )) { isAnchorAuthor = true; continue; } final char rel; final int dartSector; final String selectand; final CountNodeJS candidate = voteTrack.candidate(); if( candidate != null && candidate.name().equals( authorName )) { if( voteTrack.hasTightCycle() ) { rel = REL_TIGHT_CYCLE; selectand = DiffKey.isDartOrdered( anchor, anchorName, candidate, candidate.name() )? "a":"b"; } else { rel = REL_CANDIDATE; selectand = "a"; } dartSector = -1; // not used } else { CountNodeJS node; node = onSuccess_find( authorName, voteTrack.voters() ); if( node != null ) { rel = REL_VOTER; selectand = "b"; } else { node = onSuccess_find( authorName, voteTrack.peers() ); if( node != null ) { rel = REL_CO; selectand = DiffKey.isDartOrdered( anchor, anchorName, node, node.name() )? "a":"b"; } else // very unlikely { Stage.i().addWarning( App.i().mesS() .gwt_stage_link_NominalDifferenceTargeter_requestWarn( "author no longer in track, wiki responses maybe out of sequence", onChange_requestLoc )); continue; // hope for the best } } dartSector = node.dartSector(); } diffMap.put( authorName, new LightableDifference1( rel, dartSector, selectand, authorName, voteTrack.count().pollName() )); } if( !isAnchorAuthor ) diffMap.clear(); /* undo because there can be no differences with anchor, after all. No need to fire events or anything, this is a plain undo */ else if( diffMap.size() > 0 ) diffMap_fire(); // because it changed } finally{ retarget(); } } private CountNodeJS onSuccess_find( final String authorName, final Board board ) { CountNodeJS node; node = board.mosquito(); if( node != null && node.name().equals( authorName )) return node; for( int s = 1; s <= DART_SECTOR_MAX; ++s ) { node = board.node( s ); if( node != null && node.name().equals( authorName )) return node; } return null; } // - C h a n g e - H a n d l e r - ---------------------------------------------------- public void onChange( Change _e ) { final Board peers = voteTrack.peers(); final Board voters = voteTrack.voters(); if( !isVoteTrackReady( peers, voters )) return; // wait for it final String pollName = voteTrack.count().pollName(); final StringBuilder b = GWTX.stringBuilderClear(); final PollwikiG wiki = App.i().pollwiki(); onChange_append( b, voters, pollName, wiki ); onChange_append( b, voteTrack.peers(), pollName, wiki ); final CountNodeJS candidate = voteTrack.candidate(); if( candidate != null ) { if( voteTrack.hasTightCycle() ) assert b.length() > 0; // already appended as voter else onChange_append( b, candidate, pollName, wiki ); } else if( b.length() == 0 ) return; // anchor is orphan // [orphan? length zero seems impossible for an orphan, which would be appended // as mosquito in peer board] b.deleteCharAt( b.length() - 1 ); // remove trailing separator '|' b.insert( 0, "/api.php?titles=" ); // limit 50 titles, need 21 * 2 + 1 = 43 max b.insert( 0, PollwikiG.getScriptLocation() ); b.append( "&action=query&format=json" ); onChange_requestLoc = b.toString(); App.i().jsonp().requestObject( onChange_requestLoc, NominalDifferenceTargeter.this ); // continues at onFailure or onSuccess } private static void onChange_append( final StringBuilder b, final Board board, final String pollName, final PollwikiG wiki ) { CountNodeJS node; node = board.mosquito(); if( node != null ) onChange_append( b, node, pollName, wiki ); for( int s = 1; s <= DART_SECTOR_MAX; ++s ) { node = board.node( s ); if( node != null ) onChange_append( b, node, pollName, wiki ); } } private static void onChange_append( final StringBuilder b, final CountNodeJS node, final String pollName, final PollwikiG wiki ) { b.append( URL.encodeQueryString( wiki.positionPageName( node.name(), pollName ))); b.append( '|' ); } private String onChange_requestLoc; // - P r o p e r t y - C h a n g e - H a n d l e r ------------------------------------ public void onPropertyChange( final PropertyChange e ) { final String pName = e.propertyName(); if( pName.equals("actorName") || pName.equals("difference") ) coalescer.schedule(); else if( pName.equals( "pollName" )) { diffMap_clear(); // till sync with vote track completes coalescer.schedule(); } // eventually continues at retarget if coalescer called } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static boolean isVoteTrackReady( final Board peers, final Board voters ) { return voters.isEnabled()/*anchored*/ && peers.isEnabled()/*full track*/; } }