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}