package 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. import com.google.gson.stream.*; import java.io.IOException; import java.util.*; import java.util.regex.*; import javax.servlet.http.*; import votorola.a.count.*; import votorola.a.voter.*; import votorola.a.web.wap.*; import votorola.g.lang.*; import votorola.g.web.*; import static votorola.a.count.CountNode.DART_SECTOR_MAX; /** A web API for the {@linkplain votorola.a.count count engine}. Calls are * conventionally prefixed by 'c' (wCall=cCount). If you choose a different * prefix, then adjust the parameter names below accordingly. An example request is: * *
http://reluk.ca:8080/v/wap?wCall=cCount&cPoll=G%2Fp%2Fsandbox&cBase&wPretty
* *

Query parameters

* *

These parameters are specific to the count engine API. See also the general * {@linkplain WAP WAP} parameters.

* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
KeyValueDefault
cBaseSpecify 'cBase' or 'cBase=y' to request the base components of the count. * At present these are the baseCandidates and superaccounts.n
cGroupMailish usernames of candidates separated by left parentheses '(', as in * "cGroup=Jack-ThisOrg(Jill-ThatNet". The response will included the nodes of each specified candidate plus any direct, dart * sectored voters of that candidate.Null, optional item.
cPollNames the poll.None, a value is required.
* *

Response

* *

Long integers (64 bit) are specified in string form, allowing JavaScript clients * (normally limited to 53 bits) to parse even the largest of them by whatever extended * means is available to each. The overall response includes the following components. * These are shown in JSON format with explanatory comments:

 {
  *    "c": { // or other prefix, per {@linkplain WAP wCall} query parameter
  *
  *       "poll": { // only a single poll at present:
  *          "POLL-NAME": {
  *
  *             "error": {
  *                // Client-actionable errors.  This only appears if errors were
  *                // detected, in which case they will be the total of the response.
  *                "noCountToReport": {}
  *                   // No count is currently {@linkplain PollService#countToReport() reported} for the poll.
  *             },
  *
  *             "baseCandidates": [
  *                // Usernames of {@linkplain votorola.a.count.CountTable#BASE_CANDIDATE_TAIL base candidates}.  This only appears if requested by
  *                // parameter 'cBase'.  It names only those candidates who are {@linkplain CountNode#dartSector() dart
  *                // sectored}.  Use the names to look up the corresponding nodes.
  *                "CANDIDATE-NAME",
  *                "CANDIDATE-NAME"
  *                // and so on, up to {@value votorola.a.count.CountNode#DART_SECTOR_MAX}
  *             ],
  *
  *             "nodes": {
  *                // Count nodes keyed by username:
  *                "NAME 1": { // empty unless node actually counted, in which case:
  *                   "{@linkplain CountNode#candidateName() candidateName}": CANDIDATE NAME, // if any
  *                   "{@linkplain CountNode#dartSector() dartSector}": DART SECTOR,
  *                   "{@linkplain CountNode#directVoterCount() directVoterCount}": "DIRECT VOTER COUNT", // stringified long
  *                   "{@linkplain CountNode#displayTitle() displayTitle}": DISPLAY TITLE, // if any
  *                   "{@linkplain CountNode#isCycler() isCycler}": IS CYCLER,
  *                   "registers": {
  *                      // Superaccount registers for the node, indexed first by the page
  *                      // name of the counting method, then by the account name.  If a
  *                      // given superaccount's register is not included, then none of
  *                      // its pledges flowed to or from this node.  The "Votes" register
  *                      // is always included.
  *                      "Wiki:Vote count": {
  *                         "Votes": {
  *                            "{@linkplain CountNodeW#carryVolume() carryVolume}": "CARRY VOLUME", // stringified longs
  *                            "{@linkplain CountNodeW#receiveVolume() receiveVolume}": "RECEIVE VOLUME",
  *                            "{@linkplain CountNodeW#castVolume() castVolume}": "CAST VOLUME",
  *                            "targetVolume": "TARGET VOLUME" // optional, personally defined
  *                              // by the node.  Not yet implemented
  *                         }
  *                      },
  *                      "Wiki:Quantitive summation": {
  *                         // these registers are only simulated at present
  *                         "ACCOUNT NAME 1": {
  *                            "accountPage": "ACCOUNT", // relative path, only if an
  *                              // account defined for this node.  Not yet implemented.
  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#carryVolume() carryVolume}": "CARRY VOLUME", // stringified longs
  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#castVolume() castVolume}": "CAST VOLUME",
  *                            "{@linkplain votorola.a.count.gwt.SacRegisterJS_q#receiveVolume() receiveVolume}": "RECEIVE VOLUME",
  *                            "targetVolume": "TARGET VOLUME" // optional, personally defined
  *                              // by the node.  Not yet implemented.
  *                         },
  *                         "ACCOUNT NAME 2": {
  *                            // as above
  *                         }
  *                      }
  *                      // and so on
  *                   },
  *                   "voters": [
  *                      // Usernames of direct voters, if any.  This list only appears if
  *                      // NAME 1 was specified in parameter 'cGroup', and only if there
  *                      // are direct voters.  In that case, it names only those voters
  *                      // who are {@linkplain CountNode#dartSector() dart sectored}.  Use the names to look up the
  *                      // corresponding nodes.
  *                      "VOTER-NAME",
  *                      "VOTER-NAME"
  *                      // and so on, up to {@value votorola.a.count.CountNode#DART_SECTOR_MAX}
  *                   ]
  *                },
  *                "NAME 2" {
  *                   // as above
  *                }
  *                // and so on
  *             },
  *
  *             "superaccounts": {
  *                // Resource superaccounts for the entire count, indexed first by the
  *                // page name of the counting method, then by the account name.  This
  *                // only appears if requested by parameter 'cBase'.
  *                "Wiki:Vote count": {
  *                   "Votes": {
  *                      "{@linkplain Count#castVolume() castVolume}": "CAST VOLUME" // stringified long
  *                         // The total weight of votes cast.  This is equal to the total
  *                         // weight {@linkplain CountNodeW#holdVolume() held}.
  *                   }
  *                },
  *                "Wiki:Quantitive summation": {
  *                   "ACCOUNT NAME 1": {
  *                      "{@linkplain votorola.a.count.gwt.SacJS_q#castVolume() castVolume}": "CAST VOLUME" // stringified long
  *                   },
  *                   "ACCOUNT NAME 2": {
  *                      // as above
  *                   }
  *                   // and so on, for all superaccounts using quantitative summation
  *                }
  *             },
  *
  *             "uiString": UI-STRING
  *                // A timestamped identifier based on count.{@linkplain Count#readyDirectory() readyDirectory}().toUIString().
  *                // Combined with the poll name, it forms a unique identifier for the
  *                // count within the scope of the local vote-server.
  *          }
  *       }
  *    }
  * }
* *

For a baseline reference, here is a minimal pretty request together with the * corresponding response: * http://reluk.ca:8080/v/wap?wCall=cCount&cPoll=G%2Fp%2Fsandbox&wPretty

 {
  *    "c": {
  *       "poll": {
  *          "G/p/sandbox": {
  *             "nodes": {},
  *             "uiString": "snap-2011-54-5/readyCount-1"
  *          }
  *       }
  *    }
  * }
*/ public @ThreadRestricted("constructor") @Uncached final class CountWAP extends Call { /** Constructs a CountWAP. * * @see #prefix() * @see #req() */ public CountWAP( final String prefix, final Requesting req, final ResponseConfiguration resConfig ) throws HTTPRequestException { super( prefix, req, resConfig ); final HttpServletRequest reqHS = req.request(); final String pollName = HTTPServletRequestX.getParameterRequired( prefix() + "Poll", reqHS ); try { poll = req.wap().vsRun().scopePoll().ensurePoll( pollName ); count = poll.countToReportT(); if( count != null ) { nodes = new HashMap<>(); if( HTTPServletRequestX.getBooleanParameter( prefix() + "Base", reqHS )) { toAddBase = true; } // Base candidates. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( toAddBase ) { baseCandidates = new String[DART_SECTOR_MAX]; count.countTablePV().run( CountTable.BASE_CANDIDATE_TAIL + ' ' + CountTable.DART_SECTORED_TAIL, new CountNodeW.Runner() { private int n; public void run( final CountNodeW node ) { final String username = node.person().username(); nodesMaybePut( username, new NodeWrapper( node )); baseCandidates[n] = username; ++n; } }); } // Voters. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String spec = HTTPServletRequestX.getParameterNonEmpty( prefix() + "Group", reqHS ); if( spec != null ) { final Matcher m = DiffWAP.USERNAME_QUERY_ITEM_PATTERN.matcher( spec ); while( m.find() ) { final String groupCandidateName = m.group( 1 ); final NodeWrapper groupCandidateW; { NodeWrapper nW = nodes.get( groupCandidateName ); if( nW == null ) { nW = new NodeWrapper(); nodes.put( groupCandidateName, nW ); } else if( nW.areVotersFetched ) continue; // already known groupCandidateW = nW; } groupCandidateW.areVotersFetched = true; // or soon will be count.countTablePV().runGroup( IDPair.toInternetAddress(groupCandidateName).getAddress(), CountTable.DART_SECTORED_TAIL + ')', new CountNodeW.Runner() { private int v; public void run( final CountNodeW node ) { final String name = node.person().username(); if( name.equals( groupCandidateName )) { if( groupCandidateW.node == null ) groupCandidateW.node = node; return; } nodesMaybePut( name, new NodeWrapper(node) ); String[] voters = groupCandidateW.voters; if( voters == null ) // lazily create { voters = new String[DART_SECTOR_MAX]; groupCandidateW.voters = voters; } voters[v] = name; ++v; } }); } } } } catch( javax.mail.internet.AddressException|IOException|javax.script.ScriptException|java.sql.SQLException|javax.xml.stream.XMLStreamException x ) { throw new RuntimeException( x ); } resConfig.headNoCache(); resConfig.headMustRevalidate(); // don't use stale values } // ------------------------------------------------------------------------------------ /** The name to use in the {@link WAP wCall} query parameter, which is {@value}. For * example: wCall=cCount. */ public static final String CALL_TYPE = "Count"; // - C a l l -------------------------------------------------------------------------- public void respond( final Responding res ) throws IOException { final boolean toSimulate = false; // final boolean toSimulate = true; // TEST final JsonWriter out = res.outJSON(); out.name( prefix() ).beginObject(); out.name( "poll" ).beginObject(); out.name( poll.name() ).beginObject(); if( count == null ) { out.name( "error" ).beginObject(); out.name( "noCountToReport" ).beginObject().endObject(); out.endObject(); } else { // baseCandidates // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( toAddBase ) { out.name( "baseCandidates" ).beginArray(); for( String name: baseCandidates ) { if( name == null ) break; out.value( name ); } out.endArray(); } // nodes // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - out.name( "nodes" ).beginObject(); for( final Map.Entry entry: nodes.entrySet() ) { out.name( entry.getKey() ).beginObject(); final NodeWrapper wrap = entry.getValue(); final CountNodeW node = wrap.node; final String[] voters = wrap.voters; if( node == null ) { assert voters == null || voters.length == 0; // no need to output } else { final String candidateName = node.candidateName(); if( candidateName != null ) out.name( "candidateName" ).value( candidateName ); out.name( "dartSector" ).value( node.dartSector() ); out.name( "directVoterCount" ).value( Long.toString( node.directVoterCount() )); final String displayTitle = node.displayTitle(); if( displayTitle != null ) out.name( "displayTitle" ).value( displayTitle ); out.name( "isCycler" ).value( node.isCycler() ); out.name( "registers" ).beginObject(); out.name( "Wiki:Vote count" ).beginObject(); out.name( "Votes" ).beginObject(); out.name( "carryVolume" ).value( Long.toString( node.carryVolume() )); out.name( "receiveVolume" ).value( Long.toString( node.receiveVolume() )); out.name( "castVolume" ).value( Long.toString( node.castVolume() )); out.endObject(); // Votes out.endObject(); // Wiki:Vote count if( toSimulate ) { out.name( "Wiki:Quantitive summation" ).beginObject(); out.name( "US$" ).beginObject(); out.name( "carryVolume" ).value( Long.toString( node.carryVolume() * 3 )); out.name( "receiveVolume" ).value( Long.toString( node.receiveVolume() * 3 )); out.name( "castVolume" ).value( Long.toString( node.castVolume() * 3 )); out.endObject(); // US$ if( Poll.TEST_POLL_NAME.equals( poll.name() )) for( int n = 26; n > 0; --n ) { final char nameChar = (char)('A' + n - 1); out.name( nameChar + " test" ).beginObject(); out.name( "carryVolume" ).value( Long.toString( node.carryVolume() * n )); out.name( "receiveVolume" ).value( Long.toString( node.receiveVolume() * n )); out.name( "castVolume" ).value( Long.toString( node.castVolume() * n )); out.endObject(); } out.endObject(); // Wiki:Quantitive summation } out.endObject(); // registers if( voters != null ) { out.name( "voters" ).beginArray(); for( String name: voters ) { if( name == null ) break; out.value( name ); } out.endArray(); } } out.endObject(); // *node* } out.endObject(); // nodes // superaccounts // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( toAddBase ) { out.name( "superaccounts" ).beginObject(); out.name( "Wiki:Vote count" ).beginObject(); out.name( "Votes" ).beginObject(); out.name( "castVolume" ).value( Long.toString( count.castVolume() )); out.endObject(); // Votes out.endObject(); // Wiki:Vote count if( toSimulate ) { out.name( "Wiki:Quantitive summation" ).beginObject(); out.name( "US$" ).beginObject(); out.name( "castVolume" ).value( Long.toString( count.castVolume() * 3 )); out.endObject(); // US$ if( Poll.TEST_POLL_NAME.equals( poll.name() )) for( int n = 26; n > 0; --n ) { final char nameChar = (char)('A' + n - 1); out.name( nameChar + " test" ).beginObject(); out.name( "castVolume" ).value( Long.toString( count.castVolume() * n )); out.endObject(); } out.endObject(); // Wiki:Quantitive summation } out.endObject(); // superaccounts } // uiString // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - out.name( "uiString" ).value( count.readyDirectory().toUIString( "/" )); } out.endObject(); // *poll name* out.endObject(); // poll out.endObject(); // response } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final Count count; private String[] baseCandidates; private HashMap nodes; private void nodesMaybePut( final String username, final NodeWrapper nW ) { final NodeWrapper oldW = nodes.put( username, nW ); if( oldW != null ) nodes.put( username, oldW ); // generally rare, put it back instead of clobbering it } private final PollService poll; private boolean toAddBase; // ==================================================================================== private static final class NodeWrapper { NodeWrapper() {} NodeWrapper( CountNodeW _node ) { node = _node; } CountNodeW node; String[] voters; boolean areVotersFetched; } }