001package votorola.a.count; // Copyright 2007-2010, 2012-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 java.io.*; 004import java.sql.*; 005import java.util.*; 006import javax.mail.internet.*; 007import javax.script.*; 008import javax.xml.stream.*; 009import votorola.a.*; 010import votorola.a.trust.*; 011import votorola.a.response.*; 012import votorola.a.voter.*; 013import votorola.g.lang.*; 014import votorola.g.locale.*; 015import votorola.g.option.*; 016 017import static votorola.a.voter.IDPair.NOBODY; 018 019 020/** Responder for the command 'vote' - to cast a vote in a poll. 021 * 022 * @see <a href='../../../../../s/manual.xht#Poll-Response-vote' target='_top' 023 * >../../s/manual.xht#Poll-Response-vote</a> 024 */ 025public class CR_Vote extends CommandResponder.Base 026{ 027 028 029 /** Constructs a CR_Vote. 030 */ 031 CR_Vote( PollService _poll ) { super( _poll, "a.count.CR_Vote." ); } 032 033 034 035 /** Constructs a CR_Vote as a subclass. 036 */ 037 CR_Vote( PollService _poll, String _keyPrefix ) { super( _poll, _keyPrefix ); } 038 039 040 041 // - C o m m a n d - R e s p o n d e r ------------------------------------------------ 042 043 044 public @Override void help( final CommandResponder.Session session ) 045 { 046 final ReplyBuilder replyB = session.replyBuilder(); 047 replyB.lappendlnn( keyPrefix + "help.summary" ); 048 replyB.indent( 4 ).setWrapping( false ); 049 replyB.lappendlnn( keyPrefix + "help.syntax" ); 050 replyB.exdent( 4 ).setWrapping( true ); 051 replyB.lappendlnn( keyPrefix + "help.body(1)", voterService.title() ); 052 } 053 054 055 056 public @Override Exception respond( final String[] argv, final CommandResponder.Session s ) 057 { 058 final String commandName = argv[0]; // before rearranged by option parser 059 final AuthenticatedUser user = s.user(); 060 if( user.email().equals(NOBODY.email()) ) 061 { 062 throw new CommandResponder.AnonymousIssueException( commandName ); 063 } 064 065 final ReplyBuilder replyB = s.replyBuilder(); 066 final Map<String,Option> optionMap = compileOptions( s ); 067 final String newCandidateEmail; 068 { 069 final int aFirstNonOption = parse( argv, optionMap, s ); // rearranges argv 070 if( aFirstNonOption == -1 ) return null; // parse error, message already appended 071 072 if( optionMap.get("a.voter.CommandResponder.option.help").hasOccured() ) 073 { 074 help( s ); 075 return null; 076 } 077 078 final int nArg = argv.length - aFirstNonOption; 079 final int nArgExpected = 1; 080 if( nArg <= 0 ) newCandidateEmail = null; 081 else if( nArg == nArgExpected ) 082 { 083 try{ newCandidateEmail = canonicalEmail( argv[aFirstNonOption], commandName, s ); } 084 catch( AddressException x ){ return null; } // error message already output to reply 085 } 086 else 087 { 088 replyB.lappendln( "a.voter.CommandResponder.wrongNumberOfArguments(1,2,3)", 089 commandName, nArgExpected, nArg ); 090 replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); 091 return null; 092 } 093 } 094 final String alterEmail; 095 try 096 { 097 alterEmail = canonicalEmail( optionMap.get( "a.voter.CommandResponder.option.alter" ) 098 .argumentValue(), commandName, s ); 099 } 100 catch( AddressException x ){ return null; } // error message already output to reply 101 102 try 103 { 104 final String userEmail = user.email(); 105 final Vote vote = new Vote( alterEmail == null? userEmail:alterEmail, 106 poll().voterInputTable() ); 107 if( newCandidateEmail != null && !vote.voterEmail().equals(userEmail) ) 108 { 109 replyB.lappendln( "a.voter.CommandResponder.writePermissionDenied(1,2,3)", 110 commandName, userEmail, vote.voterEmail() ); 111 replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); 112 return null; 113 } 114 115 replyB.setWrapping( false ); 116 replyB.lappendlnn( "a.count.CR_Vote.reply.poll(1)", poll().title() ); 117 if( newCandidateEmail == null ) 118 { 119 replyB.lappendlnn( "a.count.CR_Vote.reply.candidate(1,2)", 120 IDPair.emailOrNobody( vote.getCandidateEmail(), s.bunA().bundle() ), 121 optionsToString( vote, replyB.bundle() )); 122 } 123 else writeVoteAndEcho( newCandidateEmail, vote, s ); 124 echoTraces( vote, s ); 125 } 126 catch( VoterInputTable.BadInputException x ) { replyB.appendlnn( x.toString() ); } 127 catch( IOException|ScriptException|SQLException|XMLStreamException x ) { return x; } 128 finally{ replyB.resetFormattingToDefaults(); } 129 return null; 130 } 131 132 133 134 // ==================================================================================== 135 136 137 /** A cached view of a count table restricted to a particular poll. 138 */ 139 public static @ThreadRestricted class CountTablePVC extends CountTable.PollView 140 { 141 142 // cf. ReadyDirectory.CountTablePVC 143 144 145 public CountTablePVC( final CountTable t, String _serviceName ) { t.super( _serviceName ); } 146 147 148 protected final HashMap<String,CountNodeW> cache = new HashMap<String,CountNodeW>(); 149 150 151 // -------------------------------------------------------------------------------- 152 153 154 public final @Override CountNodeW get( final String voterEmail ) 155 throws SQLException, XMLStreamException 156 { 157 CountNodeW node = cache.get( voterEmail ); 158 // if( node == null && !cache.containsKey( voterEmail )) 159 //// no need of null values 160 if( node == null ) 161 { 162 node = super.get( voterEmail ); 163 if( node != null ) cache.put( voterEmail, node ); 164 } 165 return node; 166 } 167 168 169 public final @Override CountNodeW getOrCreate( final String voterEmail ) 170 throws SQLException, XMLStreamException 171 { 172 final CountNodeW node = super.getOrCreate( voterEmail ); 173 if( node instanceof CountNodeIC ) cache.put( voterEmail, node ); 174 return node; 175 } 176 177 } 178 179 180 181 // ==================================================================================== 182 183 184 /** A pair of vote traces: the old trace as of last count, and a projected trace that 185 * takes into consideration the user's subsequent input if any. 186 * 187 * @see CountNodeW#trace() 188 */ 189 public static @ThreadSafe final class TracePair 190 { 191 192 /** Constructs a TracePair with a standard cached view of a count table. 193 */ 194 public TracePair( final PollService poll, final Count count, Vote _vote ) 195 throws IOException, ScriptException, SQLException, XMLStreamException 196 { 197 this( poll, count, _vote, new CountTablePVC( count.countTable(), poll.name() )); 198 } 199 200 201 /** Constructs a TracePair. 202 */ 203 public TracePair( PollService _poll, final Count count, final Vote vote, 204 CountTablePVC _countTablePV ) throws IOException, ScriptException, SQLException, 205 XMLStreamException 206 { 207 poll = _poll; 208 countTablePV = _countTablePV; 209 210 // Trace at last count. 211 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 212 final CountNodeW origin = countTablePV.getOrCreate( vote.voterEmail() ); 213 traceAtLastCount = origin.trace(); 214 215 // Test for bar. Cf. ReadyDirectory.mount(). 216 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 217 String bar = null; // so far 218 barTest: if( origin.isBarrable() ) 219 { 220 final TraceNodeW traceNode; 221 final Set<String> divisions; 222 { 223 final NetworkTrace trace = poll.vsRun().trustserver().traceToReportT(); 224 if( trace == null ) // no trace available 225 { 226 // let it pass, as this is only a projection 227 traceNode = null; 228 divisions = null; 229 } 230 else 231 { 232 traceNode = trace.traceNodeTable().getOrCreate( vote.voter() ); 233 if( traceNode instanceof TraceNodeIC ) // unregistered 234 { 235 poll.lock().lock(); 236 try 237 { 238 bar = (String)poll.configurationScript().invokeKnownFunction( 239 "voterBarUnregistered", vote.voter() ); 240 } 241 finally { poll.lock().unlock(); } 242 if( bar != null ) break barTest; // else unregistered voters are allowed 243 } 244 245 divisions = Collections.unmodifiableSet( 246 trace.membershipTable().divisionSet( vote.voter().email() )); 247 } 248 } 249 final VoteCastingContext vCC = new VoteCastingContext( /*isRealCount*/false, 250 traceNode, divisions ); 251 poll.lock().lock(); 252 try 253 { 254 poll.configurationScript().invokeKnownFunction( "castingVote", vCC ); 255 } 256 finally { poll.lock().unlock(); } 257 bar = vCC.getBar(); 258 } 259 260 // Projected trace. 261 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 262 final String candidateEmail = vote.getCandidateEmail(); 263 final boolean isVoteChangedSinceCount = /* if any of these variables which 264 affect the trace, or how it is reported here, have changed */ 265 !ObjectX.nullEquals( candidateEmail, origin.getCandidateEmail() ) 266 || candidateEmail != null && !ObjectX.nullEquals( bar, origin.getBar() ); 267 if( isVoteChangedSinceCount ) 268 { 269 for( int i = 0; i < traceAtLastCount.length; ++i ) 270 { 271 traceAtLastCount[i] = traceAtLastCount[i].clone(); 272 // preserving a copy, against changes below 273 } 274 origin.uncast(); // cleanly undo the results of the previous cast 275 origin.setCandidateEmail( candidateEmail ); 276 if( bar == null || origin.isBarrable() ) origin.setBar( bar ); 277 traceProjected = origin.cast(); 278 } 279 else traceProjected = null; 280 } 281 282 283 // -------------------------------------------------------------------------------- 284 285 286 /** A cached count table, holding in memory all nodes affected by the uncast/cast 287 * of the projection. 288 */ 289 public final CountTable.PollView countTablePV; 290 291 292 public final PollService poll; 293 294 295 /** The trace according to the results of last count, exclusive of any subsequent 296 * user input. 297 */ 298 public final CountNodeW[] traceAtLastCount; 299 300 301 /** A trace projected from traceAtLastCount by including any subsequent input of 302 * the user's (but not of other users); or null if the user is unknown, or had no 303 * subsequent input. 304 */ 305 public final CountNodeW[] traceProjected; 306 307 308 } 309 310 311 312//// P r i v a t e /////////////////////////////////////////////////////////////////////// 313 314 315 /** Compiles a map of launch options. 316 */ 317 static HashMap<String,Option> compileOptions( final CommandResponder.Session session ) 318 { 319 final HashMap<String,Option> optionMap = compileBaseOptions( session ); 320 final ResourceBundle bunCR = session.replyBuilder().bundle(); 321 String key; 322 323 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 324 key = "a.voter.CommandResponder.option.alter"; 325 optionMap.put( key, new Option( bunCR.getString(key), Option.REQUIRED_ARGUMENT )); 326 327 // - - - 328 return optionMap; 329 } 330 331 332 333 /** @param session the session with wrapping turned off in its reply builder. 334 */ 335 private void echoTrace( final CountNodeW[] trace, final CommandResponder.Session session ) 336 { 337 // cf. WP_Vote.newTrace() 338 339 assert trace.length > 1 == trace[0].isCast() : "origin actually cast, if it might"; 340 final ReplyBuilder replyB = session.replyBuilder(); 341 342 // Trace. 343 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 344 replyB.indent( 4 ); 345 { 346 long receiveVolumeInternal = 0; // i.e. received from previous node 347 char cLine = ' '; // continuation line from previous node 348 replyB.indent( 4 ); 349 for( int i = 0;; ) 350 { 351 final CountNodeW node = trace[i]; 352 353 // heading into node 354 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 355 final long receiveVolumeExternal = node.receiveVolume() - receiveVolumeInternal; 356 // volume entering the trace from nodes that are not part of the trace 357 if( receiveVolumeExternal > 0 ) 358 { 359 replyB.append( cLine ).appendln( " __" ); 360 replyB.append( cLine ).appendln( " /" ); 361 replyB.append( cLine ).append( "/ " ).append( receiveVolumeExternal ).appendln(); 362 replyB.appendln( '|' ); 363 } 364 if( node.receiveVolume() > 0 ) replyB.appendln( 'V' ); 365 replyB.exdent( 4 ); 366 367 // at node 368 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 369 replyB.append( node.email() ); 370 if( node.holdVolume() > 0 ) 371 { 372 replyB.append( " ---> "); 373 replyB.lappend( "a.count.CR_Vote.reply.countNode.holding(1)", 374 node.holdVolume() ); 375 } 376 replyB.appendln(); 377 ++i; 378 if( i >= trace.length ) 379 { 380 if( trace.length == 1 && node.receiveVolume() == 0 ) 381 { 382 replyB.appendln().lappendln( "a.count.CR_Vote.reply.emptyTrace" ); 383 } 384 break; 385 } 386 387 // trailing out, to next node 388 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 389 replyB.indent( 4 ); 390 replyB.appendln( '|' ); 391 receiveVolumeInternal = node.castVolume() + node.carryVolume(); 392 // i.e to be received by next node 393 cLine = '|'; 394 replyB.append( "| " ).append( receiveVolumeInternal ).appendln(); 395 replyB.appendln( '|' ); 396 } 397 } 398 replyB.exdent( 4 ); 399 replyB.appendln(); 400 401 // Eligibility bar. 402 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 403 final CountNodeW origin = trace[0]; 404 if( origin.isVoter() ) 405 { 406 final String bar = origin.getBar(); 407 if( bar != null ) 408 { 409 final BundleFormatter bunA = session.bunA(); 410 replyB.setWrapping( true ); 411 replyB.appendlnn( bunA.l( "a.count.voteBar", origin.email(), 412 origin.getCandidateEmail(), bar )); 413 replyB.setWrapping( false ); 414 } 415 } 416 } 417 418 419 420 /** Appends vote traces to the reply. Appends a first trace identical to that of the 421 * last count if any, plus a second trace adjusted for subsequent changes if any. 422 * 423 * @param session the session with wrapping turned off in its reply builder. 424 */ 425 void echoTraces( final Vote vote, final CommandResponder.Session session ) 426 throws IOException, ScriptException, SQLException, XMLStreamException 427 { 428 // cf. WP_Vote.refreshTracePair() 429 430 final BundleFormatter bunA = session.bunA(); 431 final ReplyBuilder replyB = session.replyBuilder(); 432 assert !replyB.isWrapping(); 433 final Count count = poll().countToReport(); 434 if( count == null ) 435 { 436 replyB.setWrapping( true ); 437 replyB.appendlnn( bunA.l( "a.count.noResultsToReport" )); 438 replyB.setWrapping( false ); 439 return; 440 } 441 442 final TracePair tP = new TracePair( poll(), count, vote ); 443 444 // Projected trace. 445 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 446 if( tP.traceProjected != null ) 447 { 448 echoTrace( tP.traceProjected, session ); 449 replyB.setWrapping( true ); 450 replyB.lappend( "a.count.CR_Vote.reply.finalRecipient(1)", 451 CountNodeW.finalRecipientOrNobody( tP.traceProjected, bunA.bundle() )); 452 replyB.append(" ").lappendlnn( "a.count.CR_Vote.reply.traceProjected(1)", 453 vote.voterEmail() ); 454 replyB.setWrapping( false ); 455 456 replyB.appendRepeatln( '-', ReplyBuilder.WRAPPED_WIDTH ); 457 replyB.lappendlnn( "a.count.CR_Vote.reply.traceAtLastCount" ); 458 } 459 460 // Trace at last count. 461 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 462 echoTrace( tP.traceAtLastCount, session ); 463 replyB.setWrapping( true ); 464 if( tP.traceProjected == null ) 465 { 466 replyB.lappend( "a.count.CR_Vote.reply.finalRecipient(1)", 467 CountNodeW.finalRecipientOrNobody( tP.traceAtLastCount, bunA.bundle() )); 468 replyB.append(" "); 469 } 470 final File snapDirectory = count.readyDirectory().getParentFile(); 471 // final File pollDirectory = snapDirectory.getParentFile(); 472 replyB.lappendln( "a.count.CR_Vote.reply.traceAtLastCount(1)", 473 // pollDirectory.getName() + "/" + 474 snapDirectory.getName() + " / " + count.readyDirectory().getName() ); 475 replyB.setWrapping( false ); 476 } 477 478 479 480 private static String optionsToString( Vote vote, ResourceBundle bundle ) 481 { 482 return ""; // FIX, yet to implement, per my poll.task 483 } 484 485 486 487 protected PollService poll() { return (PollService)voterService; } 488 489 490 491 /** Changes the vote and writes it to the database, and appends an echo of the change 492 * to the reply. 493 * 494 * @param session the session with wrapping turned off in its reply builder. 495 */ 496 protected void writeVoteAndEcho( final String newCandidateEmail, final Vote vote, 497 final CommandResponder.Session session ) throws SQLException, VoterInputTable.BadInputException 498 { 499 final ReplyBuilder replyB = session.replyBuilder(); 500 assert !replyB.isWrapping(); 501 502 replyB.lappendln( "a.count.CR_Vote.reply.candidateOld(1,2)", 503 IDPair.emailOrNobody( vote.getCandidateEmail(), session.bunA().bundle() ), 504 optionsToString( vote, replyB.bundle() )); 505 506 vote.setCandidateEmail( newCandidateEmail ); 507 vote.write( poll().voterInputTable(), session ); 508 replyB.lappendlnn( "a.count.CR_Vote.reply.candidateNew(1,2)", 509 IDPair.emailOrNobody( vote.getCandidateEmail(), session.bunA().bundle() ), 510 optionsToString( vote, replyB.bundle() )); 511 } 512 513 514}