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&cPoll=G%2Fp%2Fsandbox&cBase&wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=cCount&cPoll=G%2Fp%2Fsandbox&cBase&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&cPoll=G%2Fp%2Fsandbox&wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=cCount&cPoll=G%2Fp%2Fsandbox&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}