001package votorola.a.count.gwt; // Copyright 2011-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.http.client.URL; 005import com.google.gwt.jsonp.client.JsonpRequest; 006import com.google.gwt.user.client.rpc.AsyncCallback; 007import votorola.a.web.gwt.*; 008import votorola.g.web.gwt.*; 009 010 011/** A count implemented as a JavaScript overlay type. Instances of CountJS may be 012 * obtained from the {@linkplain CountCache count cache}. 013 * 014 * @see votorola.a.count.Count 015 */ 016public final class CountJS extends JavaScriptObject 017{ 018 019 020 protected CountJS() {} // "precisely one constructor... protected, empty, and no-argument" 021 022 023 024 /** Initializes a count that was received on the wire. 025 * 026 * @see #pollName() 027 */ 028 void init( final String pollName ) 029 { 030 _set( "pollName", pollName ); // fill in detail excluded from wire response 031 _set( "nodeRequestMap", createObject() ); 032 final JsMap<CountNodeJS> nodeMap = _get( "nodes" ); 033 _delete( "nodes" ); // no longer needed 034 035 // Set sparse array of end-candidate nodes from dense array of names. 036 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 037 final JsArrayString names = _get( "baseCandidates" ); 038 final JsArray<CountNodeJS> baseCandidates = createArray( CountNodeJS.DART_SECTOR_MAX ).cast(); 039 final int cN = names.length(); 040 for( int c = 0; c < cN; ++c ) 041 { 042 final String name = names.get( c ); 043 final CountNodeJS node = cachedOrWireNode( name, nodeMap ); 044 baseCandidates.set( node.dartSector() - 1, node ); 045 } 046 _set( "baseCandidates", baseCandidates ); 047 048 // Intialize the superaccounts. 049 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 050 final JsMap<CountingMethodJS<SacJS>> superaccounts = superaccounts(); 051 for( final String methodName: superaccounts._in() ) 052 { 053 if( !superaccounts._hasOwnProperty( methodName )) continue; 054 055 final CountingMethodJS<SacJS> method = superaccounts.get( methodName ); 056 for( final String accountName: method._in() ) 057 { 058 if( !method._hasOwnProperty( accountName )) continue; 059 060 final SacJS sac = method.get( accountName ); 061 try{ sac.init( methodName, accountName ); } 062 catch( final CountingMethodJS.NoSuchMethod x ) 063 { 064 assert false; 065 method._delete( accountName ); 066 // safe provided the same is done in the count nodes (CountNodeJS), 067 // because nothing else in the response refers to this superaccount 068 } 069 } 070 } 071 072 // Create the null superaccount, which is not transmitted. 073 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 074 final SacJS sac = createObject().cast(); 075 try{ sac.init( /*methodName*/"Wiki:Null count", /*accountName*/"Null" ); } 076 catch( final CountingMethodJS.NoSuchMethod x ) { throw new IllegalStateException( x ); } 077 final CountingMethodJS<SacJS> method = createObject().cast(); 078 method._set( sac.accountName(), sac ); // add the null superaccount ... 079 superaccounts._set( sac.countingMethodName(), method ); // ... under the null counting method 080 } 081 082 083 084 // ------------------------------------------------------------------------------------ 085 086 087 /** Returns a count node for the specified user, either fetching it from the cache or 088 * initializing it from the server's response and then caching it. This method 089 * serves to give precedence to any cached node, which is irreplaceable because of 090 * the possibility that it may already be referenced by a client. 091 * 092 * @param wiredNodes the "nodes" as received from {@linkplain 093 * votorola.s.wap.CountWAP CountWAP}, including that of the specified user. 094 */ 095 CountNodeJS cachedOrWireNode( final String username, final JsMap<CountNodeJS> wiredNodes ) 096 { 097 final JsMapW<NodeRequestRecord> requestMap = nodeRequestMap(); 098 final NodeRequestRecord request = requestMap.get( username ); 099 CountNodeJS node; 100 if( request != null ) 101 { 102 node = request.node(); 103 if( node != null ) return node; 104 // else assume some error in previous request, and allow it to recover thus: 105 } 106 107 node = wiredNodes.get( username ); 108 assert node != null; // eagerly or node.init &c fail to raise NullPointerException in devmode 109 node.init( username, wiredNodes, CountJS.this ); 110 requestMap.put( username, new NodeRequestRecord( node )); 111 return node; 112 } 113 114 115 116 /** A sparse array of the {@linkplain votorola.a.count.CountNode#dartSector() dart 117 * sectored} base candidates. The array is indexed from zero with null elements for 118 * empty sectors. 119 * 120 * @see <a href='../../../../../../d/theory.xht#base-candidate' 121 * target='_top'>base candidate</a> 122 */ 123 public native JsArray<CountNodeJS> baseCandidates() /*-{ return this.baseCandidates; }-*/; 124 125 // FIX to index from 1 not zero, as detailed for CountNodeJS.voters(). 126 127 128 129 /** Answers whether the node request is complete. Returns true iff a request was 130 * issued (record is non-null) that successfully fetched the node, but not its 131 * {@linkplain CountNodeJS#voters() voters}. 132 */ 133 public static boolean isNodeRequestComplete( final NodeRequestRecord r ) 134 { 135 if( r == null ) return false; 136 137 final CountNodeJS node = r.node(); 138 return node == null || node.voters() != null; 139 } 140 141 142 143 /** Returns a cached record of a node request, or null if no record is cached. 144 */ 145 public NodeRequestRecord nodeRequestRecord( final String username ) 146 { 147 return nodeRequestMap().get( username ); 148 } 149 150 151 152 /** The superaccount for the null count. 153 * 154 * @see <a href='http://reluk.ca/w/Wiki:Null_count' target='_top' 155 * >http://reluk.ca/w/Wiki:Null count</a> 156 */ 157 public native SacJS nullSuperaccount() 158 /*-{ 159 return this.superaccounts["Wiki:Null count"]["Null"]; 160 }-*/; 161 162 163 164 /** Identifies the poll that was counted. 165 */ 166 public native String pollName() /*-{ return this.pollName; }-*/; 167 168 169 170 /** Requests a node and/or its {@linkplain CountNodeJS#voters() direct voters} from 171 * the remote count engine and caches them. If the node itself is already in the 172 * cache, then the request serves only to fetch its {@linkplain CountNodeJS#voters() 173 * voters}. 174 * 175 * @param username the personal {@linkplain CountNodeJS#name() name} of the node. 176 * @param callback a receiver for the processed response. 177 * 178 * @see #requestNode_loc(String,StringBuilder) 179 */ 180 public JsonpRequest<CountCache.WAPResponse> requestNode( final String loc, final String username, 181 final AsyncCallback<CountNodeJS> callback ) 182 { 183 return App.i().jsonpWAP().requestObject( loc, new NodeCallbackRelay(username,callback) ); 184 } 185 186 187 188 /** Appends the location for calls to <code>requestNode</code> and returns the string 189 * builder. 190 */ 191 public StringBuilder requestNode_loc( final String username, final StringBuilder b ) 192 { 193 CountCache.appendRequestLoc( pollName(), b ); 194 b.append( "&cGroup=" ).append( URL.encodeQueryString( username )); 195 return b; 196 } 197 198 199 200 /** Returns the superaccount for the specified counting method and account name; or 201 * null if no such superaccount exists. 202 */ 203 public <S extends SacJS> S superaccount( final String countingMethodName, 204 final String accountName ) 205 { 206 final JsMap<CountingMethodJS<S>> methodMap = superaccounts(); 207 S sac = null; 208 final CountingMethodJS<S> method = methodMap.get( countingMethodName ); 209 if( method != null ) sac = method.get( accountName ); 210 211 return sac; 212 } 213 214 215 216 /** Resource superaccounts for the entire count, indexed first by the page name of the 217 * counting method, then by the account name. 218 * 219 * @see votorola.s.wap.CountWAP 220 */ 221 public native <S extends SacJS> JsMap<CountingMethodJS<S>> superaccounts() 222 /*-{ 223 return this.superaccounts; 224 }-*/; 225 226 227 228 /** A timestamped identifier based on count.{@linkplain 229 * votorola.a.count.Count#readyDirectory() readyDirectory}().toUIString(). 230 * 231 * @see votorola.s.wap.CountWAP 232 */ 233 public native String uiString() /*-{ return this.uiString; }-*/; 234 235 236 237 /** The vote superaccount. 238 */ 239 public native SacJS_v voteSuperaccount() 240 /*-{ 241 return this.superaccounts["Wiki:Vote count"]["Votes"]; 242 }-*/; 243 244 245 246 // - O b j e c t ------------------------------------------------------------------ 247 248 249 // /** True iff o is a CountJS with the same poll name and UI string. 250 // */ 251 // public @Override boolean equals( Object o ) 252 // { 253 // if( !( o instanceof CountJS )) return false; 254 // 255 // final CountJS other = (CountJS)o; 256 // return pollName().equals(other.pollName()) && uiString().equals(other.uiString()); 257 // 258 // } 259 //// parent method is final. Like it or not equals() is JS ===. [use Equator as in DiffLookJS] 260 261 262 263 // ==================================================================================== 264 265 266 /** A cached record of a node request. 267 */ 268 public static final class NodeRequestRecord 269 { 270 271 /** Creates a record with a null node. 272 */ 273 NodeRequestRecord() { node = null; } 274 275 276 /** Creates a record. 277 */ 278 NodeRequestRecord( final CountNodeJS _node ) { node = _node; } 279 280 281 /** The count node, or null if the request failed. At present no information 282 * concerning the cause of a failure is recorded. 283 */ 284 public CountNodeJS node() { return node; } 285 286 287 private final CountNodeJS node; 288 289 } 290 291 292 293//// P r i v a t e /////////////////////////////////////////////////////////////////////// 294 295 296 /** The cache of requested nodes for this count. 297 */ 298 private native JsMapW<NodeRequestRecord> nodeRequestMap() 299 /*-{ 300 return this.nodeRequestMap; 301 }-*/; 302 303 304 305 // ==================================================================================== 306 307 308 /** A {@linkplain #requestNode(String,NodeCallbackRelay) node request} callback that 309 * processes the response and transfers the result to a client callback. 310 */ 311 private class NodeCallbackRelay extends WAPCleanCallback 312 { 313 314 /** Creates a NodeCallbackRelay. 315 * 316 * @param _username the personal {@linkplain CountNodeJS#name() name} of the 317 * node. 318 * @param _clientCallback a receiver for the processed response. 319 */ 320 public NodeCallbackRelay( String _username, AsyncCallback<CountNodeJS> _clientCallback ) 321 { 322 super( CountJS.this.pollName() ); 323 username = _username; 324 clientCallback = _clientCallback; 325 assert !isNodeRequestComplete( nodeRequestRecord( username )): "no redundant node requests"; 326 } 327 328 329 private final AsyncCallback<CountNodeJS> clientCallback; 330 331 332 private final String username; 333 334 335 public final void onFailure( final Throwable x ) 336 { 337 final JsMapW<NodeRequestRecord> requestMap = nodeRequestMap(); 338 final NodeRequestRecord r = requestMap.get( username ); 339 if( r == null ) requestMap.put( username, new NodeRequestRecord() ); 340 else // collision with another request, or request was only for voters 341 { 342 final CountNodeJS node = r.node(); 343 if( node != null && node.voters() != null ) 344 { 345 clientCallback.onSuccess( node ); 346 return; 347 } 348 } 349 350 clientCallback.onFailure( x ); 351 } 352 353 354 public final void onSuccess( final CountCache.WAPResponseP responseP ) 355 { 356 final JsMap<CountNodeJS> wiredNodes = responseP._get( "nodes" ); 357 final CountNodeJS node = cachedOrWireNode( username, wiredNodes ); 358 if( node.voters() == null ) 359 { 360 // Response collided with a cached node for which voters are yet 361 // unfetched. Transfer the voters from this response to the cached node: 362 JsArrayString voterNames = wiredNodes.get(username)._get( "voters" ); 363 if( voterNames == null ) voterNames = createArray().cast(); 364 node.initVoters( voterNames, wiredNodes ); 365 } 366 clientCallback.onSuccess( node ); 367 } 368 } 369 370 371}