package 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. import com.google.gwt.core.client.*; import com.google.gwt.http.client.URL; import com.google.gwt.jsonp.client.JsonpRequest; import com.google.gwt.user.client.rpc.AsyncCallback; import votorola.a.web.gwt.*; import votorola.g.web.gwt.*; /** A count implemented as a JavaScript overlay type. Instances of CountJS may be * obtained from the {@linkplain CountCache count cache}. * * @see votorola.a.count.Count */ public final class CountJS extends JavaScriptObject { protected CountJS() {} // "precisely one constructor... protected, empty, and no-argument" /** Initializes a count that was received on the wire. * * @see #pollName() */ void init( final String pollName ) { _set( "pollName", pollName ); // fill in detail excluded from wire response _set( "nodeRequestMap", createObject() ); final JsMap nodeMap = _get( "nodes" ); _delete( "nodes" ); // no longer needed // Set sparse array of end-candidate nodes from dense array of names. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final JsArrayString names = _get( "baseCandidates" ); final JsArray baseCandidates = createArray( CountNodeJS.DART_SECTOR_MAX ).cast(); final int cN = names.length(); for( int c = 0; c < cN; ++c ) { final String name = names.get( c ); final CountNodeJS node = cachedOrWireNode( name, nodeMap ); baseCandidates.set( node.dartSector() - 1, node ); } _set( "baseCandidates", baseCandidates ); // Intialize the superaccounts. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final JsMap> superaccounts = superaccounts(); for( final String methodName: superaccounts._in() ) { if( !superaccounts._hasOwnProperty( methodName )) continue; final CountingMethodJS method = superaccounts.get( methodName ); for( final String accountName: method._in() ) { if( !method._hasOwnProperty( accountName )) continue; final SacJS sac = method.get( accountName ); try{ sac.init( methodName, accountName ); } catch( final CountingMethodJS.NoSuchMethod x ) { assert false; method._delete( accountName ); // safe provided the same is done in the count nodes (CountNodeJS), // because nothing else in the response refers to this superaccount } } } // Create the null superaccount, which is not transmitted. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final SacJS sac = createObject().cast(); try{ sac.init( /*methodName*/"Wiki:Null count", /*accountName*/"Null" ); } catch( final CountingMethodJS.NoSuchMethod x ) { throw new IllegalStateException( x ); } final CountingMethodJS method = createObject().cast(); method._set( sac.accountName(), sac ); // add the null superaccount ... superaccounts._set( sac.countingMethodName(), method ); // ... under the null counting method } // ------------------------------------------------------------------------------------ /** Returns a count node for the specified user, either fetching it from the cache or * initializing it from the server's response and then caching it. This method * serves to give precedence to any cached node, which is irreplaceable because of * the possibility that it may already be referenced by a client. * * @param wiredNodes the "nodes" as received from {@linkplain * votorola.s.wap.CountWAP CountWAP}, including that of the specified user. */ CountNodeJS cachedOrWireNode( final String username, final JsMap wiredNodes ) { final JsMapW requestMap = nodeRequestMap(); final NodeRequestRecord request = requestMap.get( username ); CountNodeJS node; if( request != null ) { node = request.node(); if( node != null ) return node; // else assume some error in previous request, and allow it to recover thus: } node = wiredNodes.get( username ); assert node != null; // eagerly or node.init &c fail to raise NullPointerException in devmode node.init( username, wiredNodes, CountJS.this ); requestMap.put( username, new NodeRequestRecord( node )); return node; } /** A sparse array of the {@linkplain votorola.a.count.CountNode#dartSector() dart * sectored} base candidates. The array is indexed from zero with null elements for * empty sectors. * * @see base candidate */ public native JsArray baseCandidates() /*-{ return this.baseCandidates; }-*/; // FIX to index from 1 not zero, as detailed for CountNodeJS.voters(). /** Answers whether the node request is complete. Returns true iff a request was * issued (record is non-null) that successfully fetched the node, but not its * {@linkplain CountNodeJS#voters() voters}. */ public static boolean isNodeRequestComplete( final NodeRequestRecord r ) { if( r == null ) return false; final CountNodeJS node = r.node(); return node == null || node.voters() != null; } /** Returns a cached record of a node request, or null if no record is cached. */ public NodeRequestRecord nodeRequestRecord( final String username ) { return nodeRequestMap().get( username ); } /** The superaccount for the null count. * * @see http://reluk.ca/w/Wiki:Null count */ public native SacJS nullSuperaccount() /*-{ return this.superaccounts["Wiki:Null count"]["Null"]; }-*/; /** Identifies the poll that was counted. */ public native String pollName() /*-{ return this.pollName; }-*/; /** Requests a node and/or its {@linkplain CountNodeJS#voters() direct voters} from * the remote count engine and caches them. If the node itself is already in the * cache, then the request serves only to fetch its {@linkplain CountNodeJS#voters() * voters}. * * @param username the personal {@linkplain CountNodeJS#name() name} of the node. * @param callback a receiver for the processed response. * * @see #requestNode_loc(String,StringBuilder) */ public JsonpRequest requestNode( final String loc, final String username, final AsyncCallback callback ) { return App.i().jsonpWAP().requestObject( loc, new NodeCallbackRelay(username,callback) ); } /** Appends the location for calls to requestNode and returns the string * builder. */ public StringBuilder requestNode_loc( final String username, final StringBuilder b ) { CountCache.appendRequestLoc( pollName(), b ); b.append( "&cGroup=" ).append( URL.encodeQueryString( username )); return b; } /** Returns the superaccount for the specified counting method and account name; or * null if no such superaccount exists. */ public S superaccount( final String countingMethodName, final String accountName ) { final JsMap> methodMap = superaccounts(); S sac = null; final CountingMethodJS method = methodMap.get( countingMethodName ); if( method != null ) sac = method.get( accountName ); return sac; } /** Resource superaccounts for the entire count, indexed first by the page name of the * counting method, then by the account name. * * @see votorola.s.wap.CountWAP */ public native JsMap> superaccounts() /*-{ return this.superaccounts; }-*/; /** A timestamped identifier based on count.{@linkplain * votorola.a.count.Count#readyDirectory() readyDirectory}().toUIString(). * * @see votorola.s.wap.CountWAP */ public native String uiString() /*-{ return this.uiString; }-*/; /** The vote superaccount. */ public native SacJS_v voteSuperaccount() /*-{ return this.superaccounts["Wiki:Vote count"]["Votes"]; }-*/; // - O b j e c t ------------------------------------------------------------------ // /** True iff o is a CountJS with the same poll name and UI string. // */ // public @Override boolean equals( Object o ) // { // if( !( o instanceof CountJS )) return false; // // final CountJS other = (CountJS)o; // return pollName().equals(other.pollName()) && uiString().equals(other.uiString()); // // } //// parent method is final. Like it or not equals() is JS ===. [use Equator as in DiffLookJS] // ==================================================================================== /** A cached record of a node request. */ public static final class NodeRequestRecord { /** Creates a record with a null node. */ NodeRequestRecord() { node = null; } /** Creates a record. */ NodeRequestRecord( final CountNodeJS _node ) { node = _node; } /** The count node, or null if the request failed. At present no information * concerning the cause of a failure is recorded. */ public CountNodeJS node() { return node; } private final CountNodeJS node; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** The cache of requested nodes for this count. */ private native JsMapW nodeRequestMap() /*-{ return this.nodeRequestMap; }-*/; // ==================================================================================== /** A {@linkplain #requestNode(String,NodeCallbackRelay) node request} callback that * processes the response and transfers the result to a client callback. */ private class NodeCallbackRelay extends WAPCleanCallback { /** Creates a NodeCallbackRelay. * * @param _username the personal {@linkplain CountNodeJS#name() name} of the * node. * @param _clientCallback a receiver for the processed response. */ public NodeCallbackRelay( String _username, AsyncCallback _clientCallback ) { super( CountJS.this.pollName() ); username = _username; clientCallback = _clientCallback; assert !isNodeRequestComplete( nodeRequestRecord( username )): "no redundant node requests"; } private final AsyncCallback clientCallback; private final String username; public final void onFailure( final Throwable x ) { final JsMapW requestMap = nodeRequestMap(); final NodeRequestRecord r = requestMap.get( username ); if( r == null ) requestMap.put( username, new NodeRequestRecord() ); else // collision with another request, or request was only for voters { final CountNodeJS node = r.node(); if( node != null && node.voters() != null ) { clientCallback.onSuccess( node ); return; } } clientCallback.onFailure( x ); } public final void onSuccess( final CountCache.WAPResponseP responseP ) { final JsMap wiredNodes = responseP._get( "nodes" ); final CountNodeJS node = cachedOrWireNode( username, wiredNodes ); if( node.voters() == null ) { // Response collided with a cached node for which voters are yet // unfetched. Transfer the voters from this response to the cached node: JsArrayString voterNames = wiredNodes.get(username)._get( "voters" ); if( voterNames == null ) voterNames = createArray().cast(); node.initVoters( voterNames, wiredNodes ); } clientCallback.onSuccess( node ); } } }