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}