package votorola.a.election; // Copyright 2007-2008, 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 java.io.*; import java.sql.*; import java.util.*; import javax.mail.internet.*; import javax.script.*; import javax.xml.stream.*; import votorola.a.*; import votorola.a.register.*; import votorola.a.register.VoterList; // precedence over java.util.VoterList import votorola.a.voter.*; import votorola.g.lang.*; import votorola.g.option.*; import votorola.g.sql.*; /** Responder for the command 'vote' - to cast a vote in an election. * * @see http://zelea.com/project/votorola/a/mail/guide.xht#vote */ public class CR_Vote extends CommandResponder.Base { /** Constructs a CR_Vote. */ CR_Vote( Election election ) { super( election, "a.election.CR_Vote." ); } /** Constructs a CR_Vote as a subclass. */ CR_Vote( Election election, String keyPrefix ) { super( election, keyPrefix ); } // - C o m m a n d - R e s p o n d e r ------------------------------------------------ public @Override void help( final CommandResponder.Session session ) { final ReplyBuilder replyB = session.replyBuilder(); replyB.lappendlnn( keyPrefix + "help.summary" ); replyB.indent( 4 ).setWrapping( false ); replyB.lappendlnn( keyPrefix + "help.syntax" ); replyB.exdent( 4 ).setWrapping( true ); replyB.lappendlnn( keyPrefix + "help.body(1)", electoralService.title() ); } public @Override Exception respond( final String[] argv, final CommandResponder.Session session ) { final String commandName = argv[0]; // before rearranged by option parser if( session.userEmail() == null ) throw new CommandResponder.AnonymousIssueException( commandName ); final ReplyBuilder replyB = session.replyBuilder(); final Map optionMap = compileOptions( session ); final String newCandidateEmail; { final int aFirstNonOption = parse( argv, optionMap, session ); // rearranges argv if( aFirstNonOption == -1 ) return null; // parse error, message already appended if( optionMap.get("a.voter.CommandResponder.option.help").hasOccured() ) { help( session ); return null; } final int nArg = argv.length - aFirstNonOption; final int nArgExpected = 1; if( nArg <= 0 ) newCandidateEmail = null; else if( nArg == nArgExpected ) { try { newCandidateEmail = canonicalEmail( argv[aFirstNonOption], commandName, session ); } catch( AddressException x ){ return null; } // error message already output to reply } else { replyB.lappendln( "a.voter.CommandResponder.wrongNumberOfArguments(1,2,3)", commandName, nArgExpected, nArg ); replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); return null; } } final String alterEmail; try{ alterEmail = canonicalEmail( optionMap.get( "a.voter.CommandResponder.option.alter" ).argumentValue(), commandName, session ); } catch( AddressException x ){ return null; } // error message already output to reply try { final Vote vote = new Vote( alterEmail == null? session.userEmail(): alterEmail, election().voterInputTable() ); if( newCandidateEmail != null && !vote.voterEmail().equals( session.userEmail() )) { replyB.lappendln( "a.voter.CommandResponder.writePermissionDenied(1,2,3)", commandName, session.userEmail(), vote.voterEmail() ); replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); return null; } replyB.setWrapping( false ); replyB.lappendlnn( "a.election.CR_Vote.reply.election(1)", election().title() ); if( newCandidateEmail == null ) { replyB.lappendlnn( "a.election.CR_Vote.reply.candidate(1,2)", emailOrNobody( vote.getCandidateEmail(), replyB.bundle() ), optionsToString( vote, replyB.bundle() )); } else commitVoteAndEcho( newCandidateEmail, vote, session ); // election().subserverRun().register().lock().lock(); // try // { echoTraces( vote, session ); // } // finally // { // election().subserverRun().register().lock().unlock(); // } } catch( IOException x ) { return x; } catch( NoSuchMethodException x ) { return x; } catch( ScriptException x ) { return x; } catch( SQLException x ) { return x; } catch( VoterInputTable.BadInputException x ) { replyB.appendlnn( x.toString() ); } finally { replyB.resetFormattingToDefaults(); } return null; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Changes and commits the voter's vote, * and appends an echo of the change to the reply. * * @param session with wrapping turned off in its reply builder */ protected void commitVoteAndEcho( final String newCandidateEmail, final Vote vote, final CommandResponder.Session session ) throws SQLException, VoterInputTable.BadInputException { final ReplyBuilder replyB = session.replyBuilder(); assert !replyB.isWrapping(); replyB.lappendln( "a.election.CR_Vote.reply.candidateOld(1,2)", emailOrNobody( vote.getCandidateEmail(), replyB.bundle() ), optionsToString( vote, replyB.bundle() )); vote.setCandidateEmail( newCandidateEmail ); vote.commit( election().voterInputTable(), session ); replyB.lappendlnn( "a.election.CR_Vote.reply.candidateNew(1,2)", emailOrNobody( vote.getCandidateEmail(), replyB.bundle() ), optionsToString( vote, replyB.bundle() )); } /** Compiles a map of launch options. */ static HashMap compileOptions( final CommandResponder.Session session ) { final HashMap optionMap = compileBaseOptions( session ); final ResourceBundle bundle = session.replyBuilder().bundle(); String key; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - key = "a.voter.CommandResponder.option.alter"; optionMap.put( key, new Option( bundle.getString(key), Option.REQUIRED_ARGUMENT )); // - - - return optionMap; } /** @param registration0 of origin node * @param session with wrapping turned off in its reply builder */ private void echoTrace( final CountNode[] trace, RegistrationC registration0, final CommandResponder.Session session ) throws SQLException { // cf. WP_Vote.newTrace() assert trace.length > 1 == trace[0].isCast() : "origin actually cast, if it might"; final ReplyBuilder replyB = session.replyBuilder(); // Trace. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - String bar = null; // until a bar is found, that terminates the trace replyB.indent( 4 ); { final RegistrationC[] traceRegistration = new RegistrationC[trace.length]; traceRegistration[0] = registration0; for( int i = 0; i < trace.length; ++i ) { traceRegistration[i] = new RegistrationC ( trace[i].voterEmail(), election().subserverRun().register().voterInputTable() ); } long receiveCountInternal = 0; // i.e. received from previous node replyB.indent( 4 ); for( int i = 0;; ) { final CountNode node = trace[i]; // heading into node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long receiveCountExternal = node.receiveCount() - receiveCountInternal; // votes coming into the trace, from nodes that are not part of the trace if( receiveCountExternal > 0 ) { final char cLine = receiveCountInternal == 0? ' ': '|'; // continuation line replyB.append( cLine ).appendln( " __" ); replyB.append( cLine ).appendln( " /" ); replyB.append( cLine ).append( "/ " ).append( receiveCountExternal ).appendln(); replyB.appendln( '|' ); } if( node.receiveCount() > 0 ) replyB.appendln( 'V' ); replyB.exdent( 4 ); // at node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` replyB.append( node.voterEmail() ); if( node.holdCount() > 0 ) { replyB.append( " ---> "); replyB.lappend( "a.election.CR_Vote.reply.countNode.holding(1)", node.holdCount() ); } replyB.appendln(); final RegistrationC registration = traceRegistration[i]; if( registration.getName() != null ) replyB.appendln( registration.getName() ); if( registration.getLink() != null ) replyB.appendln( registration.getLink() ); ++i; if( i >= trace.length ) { if( node.getCandidateEmail() == null || node.getBar() == null ) { if( trace.length == 1 && node.receiveCount() == 0 ) { replyB.appendln().lappendln( "a.election.CR_Vote.reply.emptyTrace" ); } break; } bar = node.getBar(); // and will break below } // trailing out, to next node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` replyB.indent( 4 ); replyB.appendln( '|' ); if( bar != null ) { replyB.appendln( '|' ); replyB.appendln( 'X' ); replyB.exdent( 4 ); break; } receiveCountInternal = node.singleCastCount() + node.carryCount(); // i.e to be received by next node replyB.append( "| " ).append( receiveCountInternal ).appendln(); replyB.appendln( '|' ); } } replyB.exdent( 4 ); replyB.appendln(); // Eligibility bar. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( bar != null ) { final CountNode finalNode = trace[trace.length-1]; replyB.setWrapping( true ); replyB.lappendlnn( "a.election.CR_Vote.reply.bar(1,2,3)", finalNode.voterEmail(), finalNode.getCandidateEmail(), bar ); replyB.setWrapping( false ); } } /** Appends vote traces to the reply. Appends a first trace identical to that * of the last count (if any); plus a second trace adjusted for subsequent changes, * if any. * * @param session with wrapping turned off in its reply builder */ void echoTraces( final Vote vote, final CommandResponder.Session session ) throws IOException, NoSuchMethodException, ScriptException, SQLException { // cf. WP_Vote.refreshTracePair() final ReplyBuilder replyB = session.replyBuilder(); assert !replyB.isWrapping(); final Count count = election().countToReport(); if( count == null ) { replyB.setWrapping( true ); replyB.lappendlnn( "a.election.noResultsToReport" ); replyB.setWrapping( false ); return; } final TracePair tP = new TracePair( election(), count, vote ); // Projected trace. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( tP.traceProjected != null ) { echoTrace( tP.traceProjected, tP.registration, session ); replyB.setWrapping( true ); replyB.lappend( "a.election.CR_Vote.reply.finalRecipient(1)", finalRecipientOrNobody( tP.traceProjected, replyB.bundle() )); replyB.append(" ").lappendlnn( "a.election.CR_Vote.reply.traceProjected(1)", vote.voterEmail() ); replyB.setWrapping( false ); replyB.appendRepeatln( '-', replyB.WRAPPED_WIDTH ); replyB.lappendlnn( "a.election.CR_Vote.reply.traceAtLastCount" ); } // Trace at last count. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - echoTrace( tP.traceAtLastCount, tP.registration, session ); replyB.setWrapping( true ); if( tP.traceProjected == null ) { replyB.lappend( "a.election.CR_Vote.reply.finalRecipient(1)", finalRecipientOrNobody( tP.traceAtLastCount, replyB.bundle() )); replyB.append(" "); } final File snapDirectory = count.readyDirectory().getParentFile(); // final File electionDirectory = snapDirectory.getParentFile(); replyB.lappendln( "a.election.CR_Vote.reply.traceAtLastCount(1)", // electionDirectory.getName() + "/" + snapDirectory.getName() + " / " + count.readyDirectory().getName() ); replyB.setWrapping( false ); } protected Election election() { return (Election)electoralService; } private static String emailOrNobody( String candidateEmail, ResourceBundle b ) { if( candidateEmail != null ) return candidateEmail; else return b.getString( "a.election.nobodyEmailPlaceholder" ); } static String finalRecipientOrNobody( CountNode[] trace, ResourceBundle b ) { CountNode finalNode = trace[trace.length-1]; String finalRecipient; if( trace.length == 1 ) finalRecipient = null; else finalRecipient = finalNode.voterEmail(); return emailOrNobody( finalRecipient, b ); } private static String optionsToString( Vote vote, ResourceBundle bundle ) { return ""; // FIX, yet to implement, per my election.task } // ==================================================================================== /** A count-node table that caches its nodes. */ static @ThreadRestricted final class CachedCountNodeTable extends CountNode.Table { // cf. ReadyDirectory.CachedCountNodeTable CachedCountNodeTable( ReadyDirectory rD, Database d ) throws IOException { super( rD, d ); } private final HashMap cache = new HashMap(); // -------------------------------------------------------------------------------- @Override CountNode get( final String voterEmail ) throws SQLException { CountNode node = cache.get( voterEmail ); // if( node == null && !cache.containsKey( voterEmail )) //// no need of null values if( node == null ) { node = super.get( voterEmail ); if( node != null ) cache.put( voterEmail, node ); } return node; } public @Override CountNode getOrCreate( final String voterEmail ) throws SQLException { final CountNode node = super.getOrCreate( voterEmail ); if( node instanceof CountNodeIC ) cache.put( voterEmail, node ); return node; } } // ==================================================================================== /** A pair of vote traces: the old trace, as of last count; * and a newly projected trace, based on the voter's subsequent input (if any). */ static @ThreadSafe final class TracePair { /** Constructs a TracePair. */ TracePair( final Election election, final Count count, final Vote vote ) throws IOException, ScriptException, SQLException { this.election = election; // Trace at last count. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final CountNode.Table countNodeTable = new CachedCountNodeTable( // cached to hold changes (if any) in memory, between uncast and cast of projection, below count.readyDirectory(), count.countNodeTable().database() ); final CountNode origin = countNodeTable.getOrCreate( vote.voterEmail() ); traceAtLastCount = origin.trace(); // Test for bar. Cf. ReadyDirectory.mount(). // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - String bar = null; // so far barTest: { final Register register = election.subserverRun().register(); final String registrationXML = register.voterInputTable().get( vote.voterEmail() ); try { // Un-registered bar? // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` // This is a common bar, and cheap to test. The test works // even if the list is temporarilly unmounted. // registration = new RegistrationC( vote.voterEmail(), registrationXML ); if( registrationXML == null ) { election.lock().lock(); try { bar = (String)election.eligibilityScript().invokeKnownFunction( "voterBarUnregistered", vote.voterEmail(), /*registerServiceEmail*/null ); // FIX, look up registerServiceEmail, using ListNode.leafRegisterPath() } finally { election.lock().unlock(); } if( bar == null ) { assert false; bar = "[voter unregistered]"; } // just in case break barTest; } // List bar? If the list is available... // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final ListNodeC listNode; { // final VoterList list = register.listToReport(); final VoterList list; { register.lock().lock(); try{ list = register.listToReport(); } finally { register.lock().unlock(); } } if( list == null ) listNode = null; // no list available, let it pass (this is only a projection) else { listNode = list.listNodeTable().getOrCreate( vote.voterEmail() ); listNode.setResidence( registration.getResidence() ); // to latest final ListIndexingContext lix = new ListIndexingContext( listNode ); register.lock().lock(); try { register.listScript().invokeKnownFunction( "indexing", lix ); } finally { register.lock().unlock(); } bar = listNode.getBar(); if( bar != null ) break barTest; } } // Election eligibility bar? // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` EligibilityContext elx = new EligibilityContext( /*isRealCount*/false, registration, listNode ); election.lock().lock(); try { bar = (String)election.eligibilityScript().invokeKnownFunction ( "voterBar", elx ); } finally { election.lock().unlock(); } // if( bar != null ) break barTest; } catch( XMLStreamException x ) { throw register.voterInputTable().newUnparseableInputException( vote.voterEmail(), registrationXML, x ); } } // Projected trace. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String candidateEmail = vote.getCandidateEmail(); final boolean isVoteChangedSinceCount = // if any of these variables, which affect the trace (or how it is reported here), have changed !ObjectX.nullEquals( candidateEmail, origin.getCandidateEmail() ) || candidateEmail != null && !ObjectX.nullEquals( bar, origin.getBar() ); if( isVoteChangedSinceCount ) { for( int i = 0; i < traceAtLastCount.length; ++i ) { traceAtLastCount[i] = traceAtLastCount[i].clone(); // preserving a copy, against changes below } origin.uncast( /*toCarry*/true ); // cleanly undo the results of the previous cast origin.setCandidateEmail( candidateEmail ); origin.setBar( bar ); traceProjected = origin.cast( /*toCarry*/true ); } else traceProjected = null; } // -------------------------------------------------------------------------------- final Election election; /** The voter's input to the register. */ final RegistrationC registration; /** Trace based on results of last count, excluding subsequent voter input. */ final CountNode[] traceAtLastCount; /** Trace projected from traceAtLastCount, by including subsequent input * from the voter (but not from other voters); or null, if there has been * no subsequent input. */ final CountNode[] traceProjected; } }