package 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. 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.trust.*; import votorola.a.response.*; import votorola.a.voter.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.option.*; import static votorola.a.voter.IDPair.NOBODY; /** Responder for the command 'vote' - to cast a vote in a poll. * * @see ../../s/manual.xht#Poll-Response-vote */ public class CR_Vote extends CommandResponder.Base { /** Constructs a CR_Vote. */ CR_Vote( PollService _poll ) { super( _poll, "a.count.CR_Vote." ); } /** Constructs a CR_Vote as a subclass. */ CR_Vote( PollService _poll, String _keyPrefix ) { super( _poll, _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)", voterService.title() ); } public @Override Exception respond( final String[] argv, final CommandResponder.Session s ) { final String commandName = argv[0]; // before rearranged by option parser final AuthenticatedUser user = s.user(); if( user.email().equals(NOBODY.email()) ) { throw new CommandResponder.AnonymousIssueException( commandName ); } final ReplyBuilder replyB = s.replyBuilder(); final Map optionMap = compileOptions( s ); final String newCandidateEmail; { final int aFirstNonOption = parse( argv, optionMap, s ); // rearranges argv if( aFirstNonOption == -1 ) return null; // parse error, message already appended if( optionMap.get("a.voter.CommandResponder.option.help").hasOccured() ) { help( s ); 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, s ); } 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, s ); } catch( AddressException x ){ return null; } // error message already output to reply try { final String userEmail = user.email(); final Vote vote = new Vote( alterEmail == null? userEmail:alterEmail, poll().voterInputTable() ); if( newCandidateEmail != null && !vote.voterEmail().equals(userEmail) ) { replyB.lappendln( "a.voter.CommandResponder.writePermissionDenied(1,2,3)", commandName, userEmail, vote.voterEmail() ); replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); return null; } replyB.setWrapping( false ); replyB.lappendlnn( "a.count.CR_Vote.reply.poll(1)", poll().title() ); if( newCandidateEmail == null ) { replyB.lappendlnn( "a.count.CR_Vote.reply.candidate(1,2)", IDPair.emailOrNobody( vote.getCandidateEmail(), s.bunA().bundle() ), optionsToString( vote, replyB.bundle() )); } else writeVoteAndEcho( newCandidateEmail, vote, s ); echoTraces( vote, s ); } catch( VoterInputTable.BadInputException x ) { replyB.appendlnn( x.toString() ); } catch( IOException|ScriptException|SQLException|XMLStreamException x ) { return x; } finally{ replyB.resetFormattingToDefaults(); } return null; } // ==================================================================================== /** A cached view of a count table restricted to a particular poll. */ public static @ThreadRestricted class CountTablePVC extends CountTable.PollView { // cf. ReadyDirectory.CountTablePVC public CountTablePVC( final CountTable t, String _serviceName ) { t.super( _serviceName ); } protected final HashMap cache = new HashMap(); // -------------------------------------------------------------------------------- public final @Override CountNodeW get( final String voterEmail ) throws SQLException, XMLStreamException { CountNodeW 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 final @Override CountNodeW getOrCreate( final String voterEmail ) throws SQLException, XMLStreamException { final CountNodeW 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 projected trace that * takes into consideration the user's subsequent input if any. * * @see CountNodeW#trace() */ public static @ThreadSafe final class TracePair { /** Constructs a TracePair with a standard cached view of a count table. */ public TracePair( final PollService poll, final Count count, Vote _vote ) throws IOException, ScriptException, SQLException, XMLStreamException { this( poll, count, _vote, new CountTablePVC( count.countTable(), poll.name() )); } /** Constructs a TracePair. */ public TracePair( PollService _poll, final Count count, final Vote vote, CountTablePVC _countTablePV ) throws IOException, ScriptException, SQLException, XMLStreamException { poll = _poll; countTablePV = _countTablePV; // Trace at last count. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final CountNodeW origin = countTablePV.getOrCreate( vote.voterEmail() ); traceAtLastCount = origin.trace(); // Test for bar. Cf. ReadyDirectory.mount(). // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - String bar = null; // so far barTest: if( origin.isBarrable() ) { final TraceNodeW traceNode; final Set divisions; { final NetworkTrace trace = poll.vsRun().trustserver().traceToReportT(); if( trace == null ) // no trace available { // let it pass, as this is only a projection traceNode = null; divisions = null; } else { traceNode = trace.traceNodeTable().getOrCreate( vote.voter() ); if( traceNode instanceof TraceNodeIC ) // unregistered { poll.lock().lock(); try { bar = (String)poll.configurationScript().invokeKnownFunction( "voterBarUnregistered", vote.voter() ); } finally { poll.lock().unlock(); } if( bar != null ) break barTest; // else unregistered voters are allowed } divisions = Collections.unmodifiableSet( trace.membershipTable().divisionSet( vote.voter().email() )); } } final VoteCastingContext vCC = new VoteCastingContext( /*isRealCount*/false, traceNode, divisions ); poll.lock().lock(); try { poll.configurationScript().invokeKnownFunction( "castingVote", vCC ); } finally { poll.lock().unlock(); } bar = vCC.getBar(); } // 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(); // cleanly undo the results of the previous cast origin.setCandidateEmail( candidateEmail ); if( bar == null || origin.isBarrable() ) origin.setBar( bar ); traceProjected = origin.cast(); } else traceProjected = null; } // -------------------------------------------------------------------------------- /** A cached count table, holding in memory all nodes affected by the uncast/cast * of the projection. */ public final CountTable.PollView countTablePV; public final PollService poll; /** The trace according to the results of last count, exclusive of any subsequent * user input. */ public final CountNodeW[] traceAtLastCount; /** A trace projected from traceAtLastCount by including any subsequent input of * the user's (but not of other users); or null if the user is unknown, or had no * subsequent input. */ public final CountNodeW[] traceProjected; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Compiles a map of launch options. */ static HashMap compileOptions( final CommandResponder.Session session ) { final HashMap optionMap = compileBaseOptions( session ); final ResourceBundle bunCR = session.replyBuilder().bundle(); String key; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - key = "a.voter.CommandResponder.option.alter"; optionMap.put( key, new Option( bunCR.getString(key), Option.REQUIRED_ARGUMENT )); // - - - return optionMap; } /** @param session the session with wrapping turned off in its reply builder. */ private void echoTrace( final CountNodeW[] trace, final CommandResponder.Session session ) { // cf. WP_Vote.newTrace() assert trace.length > 1 == trace[0].isCast() : "origin actually cast, if it might"; final ReplyBuilder replyB = session.replyBuilder(); // Trace. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - replyB.indent( 4 ); { long receiveVolumeInternal = 0; // i.e. received from previous node char cLine = ' '; // continuation line from previous node replyB.indent( 4 ); for( int i = 0;; ) { final CountNodeW node = trace[i]; // heading into node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long receiveVolumeExternal = node.receiveVolume() - receiveVolumeInternal; // volume entering the trace from nodes that are not part of the trace if( receiveVolumeExternal > 0 ) { replyB.append( cLine ).appendln( " __" ); replyB.append( cLine ).appendln( " /" ); replyB.append( cLine ).append( "/ " ).append( receiveVolumeExternal ).appendln(); replyB.appendln( '|' ); } if( node.receiveVolume() > 0 ) replyB.appendln( 'V' ); replyB.exdent( 4 ); // at node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` replyB.append( node.email() ); if( node.holdVolume() > 0 ) { replyB.append( " ---> "); replyB.lappend( "a.count.CR_Vote.reply.countNode.holding(1)", node.holdVolume() ); } replyB.appendln(); ++i; if( i >= trace.length ) { if( trace.length == 1 && node.receiveVolume() == 0 ) { replyB.appendln().lappendln( "a.count.CR_Vote.reply.emptyTrace" ); } break; } // trailing out, to next node // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` replyB.indent( 4 ); replyB.appendln( '|' ); receiveVolumeInternal = node.castVolume() + node.carryVolume(); // i.e to be received by next node cLine = '|'; replyB.append( "| " ).append( receiveVolumeInternal ).appendln(); replyB.appendln( '|' ); } } replyB.exdent( 4 ); replyB.appendln(); // Eligibility bar. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final CountNodeW origin = trace[0]; if( origin.isVoter() ) { final String bar = origin.getBar(); if( bar != null ) { final BundleFormatter bunA = session.bunA(); replyB.setWrapping( true ); replyB.appendlnn( bunA.l( "a.count.voteBar", origin.email(), origin.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 the session with wrapping turned off in its reply builder. */ void echoTraces( final Vote vote, final CommandResponder.Session session ) throws IOException, ScriptException, SQLException, XMLStreamException { // cf. WP_Vote.refreshTracePair() final BundleFormatter bunA = session.bunA(); final ReplyBuilder replyB = session.replyBuilder(); assert !replyB.isWrapping(); final Count count = poll().countToReport(); if( count == null ) { replyB.setWrapping( true ); replyB.appendlnn( bunA.l( "a.count.noResultsToReport" )); replyB.setWrapping( false ); return; } final TracePair tP = new TracePair( poll(), count, vote ); // Projected trace. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( tP.traceProjected != null ) { echoTrace( tP.traceProjected, session ); replyB.setWrapping( true ); replyB.lappend( "a.count.CR_Vote.reply.finalRecipient(1)", CountNodeW.finalRecipientOrNobody( tP.traceProjected, bunA.bundle() )); replyB.append(" ").lappendlnn( "a.count.CR_Vote.reply.traceProjected(1)", vote.voterEmail() ); replyB.setWrapping( false ); replyB.appendRepeatln( '-', ReplyBuilder.WRAPPED_WIDTH ); replyB.lappendlnn( "a.count.CR_Vote.reply.traceAtLastCount" ); } // Trace at last count. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - echoTrace( tP.traceAtLastCount, session ); replyB.setWrapping( true ); if( tP.traceProjected == null ) { replyB.lappend( "a.count.CR_Vote.reply.finalRecipient(1)", CountNodeW.finalRecipientOrNobody( tP.traceAtLastCount, bunA.bundle() )); replyB.append(" "); } final File snapDirectory = count.readyDirectory().getParentFile(); // final File pollDirectory = snapDirectory.getParentFile(); replyB.lappendln( "a.count.CR_Vote.reply.traceAtLastCount(1)", // pollDirectory.getName() + "/" + snapDirectory.getName() + " / " + count.readyDirectory().getName() ); replyB.setWrapping( false ); } private static String optionsToString( Vote vote, ResourceBundle bundle ) { return ""; // FIX, yet to implement, per my poll.task } protected PollService poll() { return (PollService)voterService; } /** Changes the vote and writes it to the database, and appends an echo of the change * to the reply. * * @param session the session with wrapping turned off in its reply builder. */ protected void writeVoteAndEcho( 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.count.CR_Vote.reply.candidateOld(1,2)", IDPair.emailOrNobody( vote.getCandidateEmail(), session.bunA().bundle() ), optionsToString( vote, replyB.bundle() )); vote.setCandidateEmail( newCandidateEmail ); vote.write( poll().voterInputTable(), session ); replyB.lappendlnn( "a.count.CR_Vote.reply.candidateNew(1,2)", IDPair.emailOrNobody( vote.getCandidateEmail(), session.bunA().bundle() ), optionsToString( vote, replyB.bundle() )); } }