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}