001package votorola.s.wap; // 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.gson.stream.*;
004import java.io.IOException;
005import java.util.*;
006import java.util.regex.*;
007import javax.servlet.http.*;
008import votorola.a.count.*;
009import votorola.a.voter.*;
010import votorola.a.web.wap.*;
011import votorola.g.lang.*;
012import votorola.g.web.*;
013
014import static votorola.a.count.CountNode.DART_SECTOR_MAX;
015
016
017/** A web API for the {@linkplain votorola.a.count count engine}.  Calls are
018  * conventionally prefixed by 'c' (<code>wCall=cCount</code>).  If you choose a different
019  * prefix, then adjust the parameter names below accordingly.  An example request is:
020  *
021  * <blockquote><code><a href='http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;cBase&amp;wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;cBase&amp;wPretty</a></code></blockquote>
022  *
023  * <h3>Query parameters</h3>
024  *
025  * <p>These parameters are specific to the count engine API.  See also the general
026  * {@linkplain WAP WAP} parameters.</p>
027  *
028  * <table class='definition' style='margin-left:1em'>
029  *     <tr>
030  *         <th class='key'>Key</th>
031  *         <th>Value</th>
032  *         <th>Default</th>
033  *         </tr>
034  *     <tr><td class='key'>cBase</td>
035  *
036  *         <td>Specify 'cBase' or 'cBase=y' to request the base components of the count.
037  *         At present these are the <a href='#baseCandidates'>baseCandidates</a> and <a href='#superaccounts'>superaccounts</a>.</td>
038  *
039  *         <td>n</td>
040  *
041  *         </tr>
042  *     <tr><td class='key'>cGroup</td>
043  *
044  *         <td>Mailish usernames of candidates separated by left parentheses '(', as in
045  *         "cGroup=Jack-ThisOrg(Jill-ThatNet". The response will included the <a href='#nodes''>nodes</a> of each specified candidate plus any direct, dart
046  *         sectored <a href='#voters'>voters</a> of that candidate.</td>
047  *
048  *         <td>Null, optional item.</td>
049  *
050  *         </tr>
051  *     <tr><td class='key'>cPoll</td>
052  *
053  *         <td>Names the <a href='http://reluk.ca/w/Category:Poll' target='_top'>poll</a>.</td>
054  *
055  *         <td>None, a value is required.</td>
056  *
057  *         </tr>
058  *     </table>
059  *
060  * <h3>Response</h3>
061  *
062  * <p>Long integers (64 bit) are specified in string form, allowing JavaScript clients
063  * (normally limited to 53 bits) to parse even the largest of them by whatever extended
064  * means is available to each.  The overall response includes the following components.
065  * These are shown in JSON format with explanatory comments:</p><pre
066  *
067 *> {
068  *    "c": { // or other prefix, per {@linkplain WAP wCall} query parameter
069  *
070  *       "poll": { // only a single poll at present:
071  *          "POLL-NAME": {
072  *
073  *             "error": {
074  *                // Client-actionable errors.  This only appears if errors were
075  *                // detected, in which case they will be the total of the response.
076  *                "noCountToReport": {}
077  *                   // No count is currently {@linkplain PollService#countToReport() reported} for the poll.
078  *             },
079  *
080  *             "<span id='baseCandidates'>baseCandidates</span>": [
081  *                // Usernames of {@linkplain votorola.a.count.CountTable#BASE_CANDIDATE_TAIL base candidates}.  This only appears if requested by
082  *                // parameter 'cBase'.  It names only those candidates who are {@linkplain CountNode#dartSector() dart
083  *                // sectored}.  Use the names to look up the corresponding <a href='#nodes'>nodes</a>.
084  *                "CANDIDATE-NAME",
085  *                "CANDIDATE-NAME"
086  *                // and so on, up to {@value votorola.a.count.CountNode#DART_SECTOR_MAX}
087  *             ],
088  *
089  *             "<span id='nodes'>nodes</span>": {
090  *                // Count nodes keyed by username:
091  *                "NAME 1": { // empty unless node actually counted, in which case:
092  *                   "{@linkplain CountNode#candidateName() candidateName}": CANDIDATE NAME, // if any
093  *                   "{@linkplain CountNode#dartSector() dartSector}": DART SECTOR,
094  *                   "{@linkplain CountNode#directVoterCount() directVoterCount}": "DIRECT VOTER COUNT", // stringified long
095  *                   "{@linkplain CountNode#displayTitle() displayTitle}": DISPLAY TITLE, // if any
096  *                   "{@linkplain CountNode#isCycler() isCycler}": IS CYCLER,
097  *                   "registers": {
098  *                      // <a href='http://reluk.ca/w/Category:Account#Superaccounts' target='_top'>Superaccount</a> registers for the node, indexed first by the page
099  *                      // name of the counting method, then by the account name.  If a
100  *                      // given superaccount's register is not included, then none of
101  *                      // its pledges flowed to or from this node.  The "Votes" register
102  *                      // is always included.
103  *                      "Wiki:Vote count": {
104  *                         "Votes": {
105  *                            "{@linkplain CountNodeW#carryVolume() carryVolume}": "CARRY VOLUME", // stringified longs
106  *                            "{@linkplain CountNodeW#receiveVolume() receiveVolume}": "RECEIVE VOLUME",
107  *                            "{@linkplain CountNodeW#castVolume() castVolume}": "CAST VOLUME",
108  *                            "targetVolume": "TARGET VOLUME" // optional, personally defined
109  *                              // by the node.  Not yet implemented
110  *                         }
111  *                      },
112  *                      "Wiki:Quantitive summation": {
113  *                         // these registers are only simulated at present
114  *                         "ACCOUNT NAME 1": {
115  *                            "accountPage": "ACCOUNT", // relative path, only if an
116  *                              // account defined for this node.  Not yet implemented.
117  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#carryVolume() carryVolume}": "CARRY VOLUME", // stringified longs
118  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#castVolume() castVolume}": "CAST VOLUME",
119  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#receiveVolume() receiveVolume}": "RECEIVE VOLUME",
120  *                            "targetVolume": "TARGET VOLUME" // optional, personally defined
121  *                              // by the node.  Not yet implemented.
122  *                         },
123  *                         "ACCOUNT NAME 2": {
124  *                            // as above
125  *                         }
126  *                      }
127  *                      // and so on
128  *                   },
129  *                   "<span id='voters'>voters</span>": [
130  *                      // Usernames of direct voters, if any.  This list only appears if
131  *                      // NAME 1 was specified in parameter 'cGroup', and only if there
132  *                      // are direct voters.  In that case, it names only those voters
133  *                      // who are {@linkplain CountNode#dartSector() dart sectored}.  Use the names to look up the
134  *                      // corresponding <a href='#nodes'>nodes</a>.
135  *                      "VOTER-NAME",
136  *                      "VOTER-NAME"
137  *                      // and so on, up to {@value votorola.a.count.CountNode#DART_SECTOR_MAX}
138  *                   ]
139  *                },
140  *                "NAME 2" {
141  *                   // as above
142  *                }
143  *                // and so on
144  *             },
145  *
146  *             "<span id='superaccounts'>superaccounts</span>": {
147  *                // Resource <a href='http://reluk.ca/w/Category:Account#Superaccounts' target='_top'>superaccounts</a> for the entire count, indexed first by the
148  *                // page name of the counting method, then by the account name.  This
149  *                // only appears if requested by parameter 'cBase'.
150  *                "Wiki:Vote count": {
151  *                   "Votes": {
152  *                      "{@linkplain Count#castVolume() castVolume}": "CAST VOLUME" // stringified long
153  *                         // The total weight of votes cast.  This is equal to the total
154  *                         // weight {@linkplain CountNodeW#holdVolume() held}.
155  *                   }
156  *                },
157  *                "Wiki:Quantitive summation": {
158  *                   "ACCOUNT NAME 1": {
159  *                      "{@linkplain votorola.a.count.gwt.SacJS_q#castVolume() castVolume}": "CAST VOLUME" // stringified long
160  *                   },
161  *                   "ACCOUNT NAME 2": {
162  *                      // as above
163  *                   }
164  *                   // and so on, for all superaccounts using quantitative summation
165  *                }
166  *             },
167  *
168  *             "uiString": UI-STRING
169  *                // A timestamped identifier based on count.{@linkplain Count#readyDirectory() readyDirectory}().toUIString().
170  *                // Combined with the poll name, it forms a unique identifier for the
171  *                // count within the scope of the local vote-server.
172  *          }
173  *       }
174  *    }
175  * }</pre>
176  *
177  * <p>For a baseline reference, here is a minimal pretty request together with the
178  * corresponding response:
179  * <code><a href='http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;wPretty</a></code></p><pre
180 *> {
181  *    "c": {
182  *       "poll": {
183  *          "G/p/sandbox": {
184  *             "nodes": {},
185  *             "uiString": "snap-2011-54-5/readyCount-1"
186  *          }
187  *       }
188  *    }
189  * }</pre>
190  */
191public @ThreadRestricted("constructor") @Uncached final class CountWAP extends Call
192{
193
194
195    /** Constructs a CountWAP.
196      *
197      *     @see #prefix()
198      *     @see #req()
199      */
200    public CountWAP( final String prefix, final Requesting req,
201      final ResponseConfiguration resConfig ) throws HTTPRequestException
202    {
203        super( prefix, req, resConfig );
204        final HttpServletRequest reqHS = req.request();
205        final String pollName = HTTPServletRequestX.getParameterRequired( prefix() + "Poll", reqHS );
206        try
207        {
208            poll = req.wap().vsRun().scopePoll().ensurePoll( pollName );
209            count = poll.countToReportT();
210            if( count != null )
211            {
212                nodes = new HashMap<>();
213                if( HTTPServletRequestX.getBooleanParameter( prefix() + "Base", reqHS ))
214                {
215                    toAddBase = true;
216                }
217
218              // Base candidates.
219              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
220                if( toAddBase )
221                {
222                    baseCandidates = new String[DART_SECTOR_MAX];
223                    count.countTablePV().run( CountTable.BASE_CANDIDATE_TAIL + ' '
224                      + CountTable.DART_SECTORED_TAIL, new CountNodeW.Runner()
225                    {
226                        private int n;
227                        public void run( final CountNodeW node )
228                        {
229                            final String username = node.person().username();
230                            nodesMaybePut( username, new NodeWrapper( node ));
231                            baseCandidates[n] = username;
232                            ++n;
233                        }
234                    });
235                }
236
237              // Voters.
238              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
239                final String spec = HTTPServletRequestX.getParameterNonEmpty( prefix() + "Group",
240                  reqHS );
241                if( spec != null )
242                {
243                    final Matcher m = DiffWAP.USERNAME_QUERY_ITEM_PATTERN.matcher( spec );
244                    while( m.find() )
245                    {
246                        final String groupCandidateName = m.group( 1 );
247                        final NodeWrapper groupCandidateW;
248                        {
249                            NodeWrapper nW = nodes.get( groupCandidateName );
250                            if( nW == null )
251                            {
252                                nW = new NodeWrapper();
253                                nodes.put( groupCandidateName, nW );
254                            }
255                            else if( nW.areVotersFetched ) continue; // already known
256
257                            groupCandidateW = nW;
258                        }
259                        groupCandidateW.areVotersFetched = true; // or soon will be
260                        count.countTablePV().runGroup(
261                          IDPair.toInternetAddress(groupCandidateName).getAddress(),
262                          CountTable.DART_SECTORED_TAIL + ')', new CountNodeW.Runner()
263                        {
264                            private int v;
265                            public void run( final CountNodeW node )
266                            {
267                                final String name = node.person().username();
268                                if( name.equals( groupCandidateName ))
269                                {
270                                    if( groupCandidateW.node == null ) groupCandidateW.node = node;
271                                    return;
272                                }
273
274                                nodesMaybePut( name, new NodeWrapper(node) );
275                                String[] voters = groupCandidateW.voters;
276                                if( voters == null ) // lazily create
277                                {
278                                    voters = new String[DART_SECTOR_MAX];
279                                    groupCandidateW.voters = voters;
280                                }
281                                voters[v] = name;
282                                ++v;
283                            }
284                        });
285                    }
286                }
287            }
288        }
289        catch( javax.mail.internet.AddressException|IOException|javax.script.ScriptException|java.sql.SQLException|javax.xml.stream.XMLStreamException x )
290        {
291            throw new RuntimeException( x );
292        }
293
294        resConfig.headNoCache();
295        resConfig.headMustRevalidate(); // don't use stale values
296    }
297
298
299
300   // ------------------------------------------------------------------------------------
301
302
303    /** The name to use in the {@link WAP wCall} query parameter, which is {@value}.  For
304      * example: <code>wCall=cCount</code>.
305      */
306    public static final String CALL_TYPE = "Count";
307
308
309
310   // - C a l l --------------------------------------------------------------------------
311
312
313    public void respond( final Responding res ) throws IOException
314    {
315        final boolean toSimulate = false;
316     // final boolean toSimulate = true; // TEST
317        final JsonWriter out = res.outJSON();
318        out.name( prefix() ).beginObject();
319        out.name( "poll" ).beginObject();
320        out.name( poll.name() ).beginObject();
321        if( count == null )
322        {
323            out.name( "error" ).beginObject();
324            out.name( "noCountToReport" ).beginObject().endObject();
325            out.endObject();
326        }
327        else
328        {
329          // baseCandidates
330          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
331            if( toAddBase )
332            {
333                out.name( "baseCandidates" ).beginArray();
334                for( String name: baseCandidates )
335                {
336                    if( name == null ) break;
337
338                    out.value( name );
339                }
340                out.endArray();
341            }
342
343          // nodes
344          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
345            out.name( "nodes" ).beginObject();
346            for( final Map.Entry<String,NodeWrapper> entry: nodes.entrySet() )
347            {
348                out.name( entry.getKey() ).beginObject();
349                final NodeWrapper wrap = entry.getValue();
350                final CountNodeW node = wrap.node;
351                final String[] voters = wrap.voters;
352                if( node == null )
353                {
354                    assert voters == null || voters.length == 0; // no need to output
355                }
356                else
357                {
358                    final String candidateName = node.candidateName();
359                    if( candidateName != null ) out.name( "candidateName" ).value( candidateName );
360                    out.name( "dartSector" ).value( node.dartSector() );
361                    out.name( "directVoterCount" ).value( Long.toString( node.directVoterCount() ));
362                    final String displayTitle = node.displayTitle();
363                    if( displayTitle != null ) out.name( "displayTitle" ).value( displayTitle );
364                    out.name( "isCycler" ).value( node.isCycler() );
365                    out.name( "registers" ).beginObject();
366                    out.name( "Wiki:Vote count" ).beginObject();
367                    out.name( "Votes" ).beginObject();
368                    out.name( "carryVolume" ).value( Long.toString( node.carryVolume() ));
369                    out.name( "receiveVolume" ).value( Long.toString( node.receiveVolume() ));
370                    out.name( "castVolume" ).value( Long.toString( node.castVolume() ));
371                    out.endObject(); // Votes
372                    out.endObject(); // Wiki:Vote count
373                    if( toSimulate )
374                    {
375                        out.name( "Wiki:Quantitive summation" ).beginObject();
376                        out.name( "US$" ).beginObject();
377                        out.name( "carryVolume" ).value( Long.toString( node.carryVolume() * 3 ));
378                        out.name( "receiveVolume" ).value( Long.toString(
379                          node.receiveVolume() * 3 ));
380                        out.name( "castVolume" ).value( Long.toString( node.castVolume() * 3 ));
381                        out.endObject(); // US$
382                        if( Poll.TEST_POLL_NAME.equals( poll.name() )) for( int n = 26; n > 0; --n )
383                        {
384                            final char nameChar = (char)('A' + n - 1);
385                            out.name( nameChar + " test" ).beginObject();
386                            out.name( "carryVolume" ).value( Long.toString(
387                              node.carryVolume() * n ));
388                            out.name( "receiveVolume" ).value( Long.toString(
389                              node.receiveVolume() * n ));
390                            out.name( "castVolume" ).value( Long.toString( node.castVolume() * n ));
391                            out.endObject();
392                        }
393                        out.endObject(); // Wiki:Quantitive summation
394                    }
395                    out.endObject(); // registers
396                    if( voters != null )
397                    {
398                        out.name( "voters" ).beginArray();
399                        for( String name: voters )
400                        {
401                            if( name == null ) break;
402
403                            out.value( name );
404                        }
405                        out.endArray();
406                    }
407                }
408                out.endObject(); // *node*
409            }
410            out.endObject(); // nodes
411
412          // superaccounts
413          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
414            if( toAddBase )
415            {
416                out.name( "superaccounts" ).beginObject();
417                out.name( "Wiki:Vote count" ).beginObject();
418                out.name( "Votes" ).beginObject();
419                out.name( "castVolume" ).value( Long.toString( count.castVolume() ));
420                out.endObject(); // Votes
421                out.endObject(); // Wiki:Vote count
422                if( toSimulate )
423                {
424                    out.name( "Wiki:Quantitive summation" ).beginObject();
425                    out.name( "US$" ).beginObject();
426                    out.name( "castVolume" ).value( Long.toString( count.castVolume() * 3 ));
427                    out.endObject(); // US$
428                    if( Poll.TEST_POLL_NAME.equals( poll.name() )) for( int n = 26; n > 0; --n )
429                    {
430                        final char nameChar = (char)('A' + n - 1);
431                        out.name( nameChar + " test" ).beginObject();
432                        out.name( "castVolume" ).value( Long.toString( count.castVolume() * n ));
433                        out.endObject();
434                    }
435                    out.endObject(); // Wiki:Quantitive summation
436                }
437                out.endObject(); // superaccounts
438            }
439
440          // uiString
441          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
442            out.name( "uiString" ).value( count.readyDirectory().toUIString( "/" ));
443        }
444
445        out.endObject(); // *poll name*
446        out.endObject(); // poll
447        out.endObject(); // response
448    }
449
450
451
452//// P r i v a t e ///////////////////////////////////////////////////////////////////////
453
454
455    private final Count count;
456
457
458
459    private String[] baseCandidates;
460
461
462
463    private HashMap<String,NodeWrapper> nodes;
464
465
466
467    private void nodesMaybePut( final String username, final NodeWrapper nW )
468    {
469        final NodeWrapper oldW = nodes.put( username, nW );
470        if( oldW != null ) nodes.put( username, oldW );
471          // generally rare, put it back instead of clobbering it
472    }
473
474
475
476    private final PollService poll;
477
478
479
480    private boolean toAddBase;
481
482
483
484   // ====================================================================================
485
486
487    private static final class NodeWrapper
488    {
489
490        NodeWrapper() {}
491
492
493        NodeWrapper( CountNodeW _node ) { node = _node; }
494
495
496        CountNodeW node;
497
498
499        String[] voters;
500
501
502        boolean areVotersFetched;
503
504    }
505
506
507}