001package votorola.a.count; // Copyright 2007-2012, 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 javax.xml.stream.*;
006import votorola.a.*;
007import votorola.a.voter.*;
008import votorola.g.*;
009import votorola.g.lang.*;
010import votorola.g.logging.*;
011import votorola.g.sql.*;
012
013import static votorola.a.voter.IDPair.NOBODY;
014
015
016/** A voter's input to a poll backed by a row of the input table.
017  *
018  *     @see InputTable
019  */
020public @ThreadRestricted("touch") final class Vote implements Cloneable, Serializable
021{
022
023
024    private static final long serialVersionUID = 3L;
025
026
027
028    /** Constructs a Vote from an ID pair, reading the initial state from the input table.
029      */
030    public Vote( IDPair _voter, final VoterInputTable<?> voterInputTable ) throws SQLException
031    {
032        this( _voter );
033        assert voterInputTable.voterService() instanceof PollService;
034
035        final String xml = voterInputTable.get( voter.email() );
036        if( xml == null ) LoggerX.i(getClass()).finest( "stored input is null, assuming defaults: " + voter.email() );
037        else
038        {
039            try{ init( xml ); }
040            catch( final XMLStreamException x )
041            {
042                throw voterInputTable.newUnparseableInputException( voter.email(), xml, x );
043            }
044        }
045    }
046
047
048
049    /** Constructs a Vote from an email address, reading the initial state from the input
050      * table.
051      */
052    public Vote( final String voterEmail, VoterInputTable<?> _voterInputTable ) throws SQLException
053    {
054        this( IDPair.fromEmail(voterEmail), _voterInputTable );
055    }
056
057
058
059    /** Constructs a Vote from the specified initial data.
060      *
061      *     @param xml the initial data from the '{@linkplain VoterInputTable#get(String)
062      *       xml}' column of the input table, or null to use defaults.
063      */
064    Vote( final IDPair voter, final String xml ) throws XMLStreamException
065    {
066        this( voter );
067        if( xml != null ) init( xml );
068    }
069
070
071
072    /** Constructs a Vote from the specified initial data.
073      *
074      *     @param xml the initial data from the '{@linkplain VoterInputTable#get(String)
075      *       xml}' column of the input table, or null to use defaults.
076      */
077    public Vote( final String voterEmail, final String xml ) throws XMLStreamException
078    {
079        this( IDPair.fromEmail(voterEmail), xml );
080    }
081
082
083
084    /** Constructs a Vote with default initial data.
085      */
086    public Vote( IDPair _voter )
087    {
088        if( _voter == null ) throw new NullPointerException(); // fail fast
089
090        voter = _voter;
091    }
092
093
094
095    /** Constructs a Vote with default initial data.
096      */
097    public Vote( final String voterEmail ) { this( IDPair.fromEmail( voterEmail )); }
098
099
100
101    private void init( final String xml ) throws XMLStreamException
102    {
103        final StringReader rS = new StringReader( xml );
104        try
105        {
106            final XMLStreamReader r = XMLColumnAppender.newStreamReader( rS );
107            try
108            {
109                while( r.hasNext() )
110                {
111                    r.next();
112                    if( r.isStartElement() && "X".equals( r.getLocalName() ))
113                    {
114                        candidate = IDPair.fromEmail( r.getAttributeValue( /*namespaceURI*/null,
115                          "c" ));
116                        dartSector = XMLColumnAppender.stringToByte(
117                          r.getAttributeValue( /*namespaceURI*/null, "dS" ));
118                        time = XMLColumnAppender.stringToLong(
119                          r.getAttributeValue( /*namespaceURI*/null, "t" ));
120                        break;
121                    }
122                }
123            }
124            finally{ r.close(); }
125        }
126        finally{ rS.close(); }
127    }
128
129
130
131   // ------------------------------------------------------------------------------------
132
133
134   // Ensure that any field a Wicket form might want to access is provided with an
135   // accessor method that is declared public (package is not suffient).  See
136   // votorola.a.web.wic.VPage.
137
138
139    /** The candidate for whom the voter is voting, or {@linkplain IDPair#NOBODY NOBODY}
140      * if the voter's vote is currently uncast.
141      *
142      *     @see #setCandidate(IDPair)
143      */
144    public IDPair getCandidate() { return candidate; }
145
146
147        private IDPair candidate = NOBODY;
148
149
150
151        /** Changes the candidate for whom the voter is voting, and resets the timestamp.
152          *
153          *     @see #getCandidate()
154          *     @see #resetTime()
155          */
156        public void setCandidate( final IDPair newCandidate )
157        {
158            if( newCandidate == null ) throw new NullPointerException(); // fail fast
159
160            if( candidate.equals( newCandidate )) return;
161
162            candidate = newCandidate;
163            isChanged = true;
164            resetTime();
165        }
166
167
168
169    /** Identifies the candidate for whom the voter is voting.  This method is provided
170      * for backward compatibility and will soon be deprecated.
171      *
172      *     @return the canonical email address of the candidate, or null if the vote is
173      *         currently uncast.
174      */
175    public String getCandidateEmail()
176    {
177        final IDPair c = candidate; // snapshot copy, for atomic test/return
178        return c.equals(NOBODY)? null: c.email();
179    }
180
181
182
183        /** Changes the candidate for whom the voter is voting, and resets the timestamp.
184          * This method is provided for backward compatibility, and
185          * will soon be deprecated.
186          */
187        public void setCandidateEmail( final String newCandidateEmail )
188        {
189            setCandidate( IDPair.fromEmail( newCandidateEmail ));
190        }
191
192
193
194    /** Returns the dart sector that was last assigned by a count, if any.
195      *
196      *     @return a dart sector of 1 to {@value
197      *       votorola.a.count.CountNode#DART_SECTOR_MAX}, or zero if no dart
198      *       sector has been assigned.
199      *
200      *     @see #setDartSector(int)
201      *     @see CountNode#dartSector()
202      */
203    public byte getDartSector() { return dartSector; }
204
205
206        private byte dartSector;
207
208
209
210        /** Sets the dart sector.
211          *
212          *     @see #getDartSector()
213          */
214        public void setDartSector( int _newSector )
215        {
216            final byte newSector = (byte)_newSector;
217            if( newSector == dartSector ) return;
218
219            dartSector = newSector;
220            isChanged = true;
221        }
222
223
224
225    /** Specifies the time at which the voter last altered this vote.
226      *
227      *     @return the time in milliseconds since the Epoch, or 0L if the time is
228      *       unknown, or if this vote has never been altered by the voter.
229      *
230      *     @see #getCandidateEmail()
231      *     @see #setTime(long)
232      */
233    public long getTime() { return time; }
234
235
236        private long time;
237
238
239
240        /** Sets the time at which the voter last altered this vote to the current clock
241          * time.
242          *
243          *     @see #getTime()
244          */
245        public void resetTime() { setTime( System.currentTimeMillis() ); }
246
247
248
249        /** Sets the time at which the voter last altered this vote.
250          *
251          *     @see #getTime()
252          */
253        public void setTime( final long newTime )
254        {
255            if( newTime == time ) return;
256
257            time = newTime;
258            isChanged = true;
259        }
260
261
262
263    /** The voter.
264      */
265    public IDPair voter() { return voter; }
266
267
268        private final IDPair voter;
269
270
271
272    /** Identifies the voter.  This method is provided for backward compatibility, and
273      * will soon be deprecated.
274      *
275      *     @return the canonical email address.
276      *
277      *     @see votorola.g.mail.InternetAddressX#canonicalAddress(String)
278      */
279    public String voterEmail() { return voter.email(); }
280
281
282
283    /** Writes this vote to the table if it has unwritten changes, or deletes the vote if
284      * it's at default.
285      *
286      *     @param userSession the session of the user requesting the change, or null if
287      *       the change is not user requested.
288      */
289    void write( VoterInputTable<?> voterInputTable, ServiceSession userSession )
290      throws SQLException, VoterInputTable.BadInputException
291    {
292        write( voterInputTable, userSession, /*toForce*/false );
293    }
294
295
296
297    /** Writes this vote to the table, or deletes the vote if it's at default.
298      *
299      *     @param toForce false to write only if changes were made; true to force the
300      *       write regardless.
301      *     @param userSession the session of the user requesting the change, or null if
302      *       the change is not user requested.
303      */
304    public void write( final VoterInputTable<?> voterInputTable, final ServiceSession userSession,
305      final boolean toForce ) throws SQLException, VoterInputTable.BadInputException
306    {
307        if( !( toForce || isChanged )) return;
308
309        if( voter.equals( NOBODY )) throw new IllegalArgumentException();
310          // you may create nobody's vote if you don't like to model it as null, but you
311          // may not store it
312
313        final StringBuilder b = new StringBuilder();
314        {
315            final VoterInputTable.XMLColumnBuilder bC = new VoterInputTable.XMLColumnBuilder( b );
316            bC.appendAttribute( "c", getCandidateEmail() );
317            bC.appendAttribute( "dS", XMLColumnAppender.byteToString(dartSector) );
318            bC.appendAttribute( "t", XMLColumnAppender.longToString(time) );
319        }
320        isChanged = false; // early, in case clobbering another thread's true - isChanged must be volatile for this
321        try
322        {
323            if( b.length() == 0 )
324            {
325                LoggerX.i(getClass()).finest( "deleting storage, all data is at default" );
326                voterInputTable.delete( voterEmail() );
327            }
328            else
329            {
330                b.insert( 0, "<X" ).append( "/>" );
331                voterInputTable.put( voterEmail(), b.toString(), userSession );
332            }
333        }
334        catch( RuntimeException x ) { isChanged = true; throw x; } // rollback
335        catch( SQLException x ) { isChanged = true; throw x; }
336    }
337
338
339
340   // - O b j e c t ----------------------------------------------------------------------
341
342
343    public final @Override Vote clone()
344    {
345        try
346        {
347            return (Vote)super.clone();
348        }
349        catch( CloneNotSupportedException x ) { throw new RuntimeException( x ); } // never occurs
350    }
351
352
353
354   // ====================================================================================
355
356
357    /** An event that records the casting of a vote.
358      */
359    public static final class CastEvent extends VotingEvent
360    {
361
362        /** Constructs a CastEvent.
363          */
364        CastEvent( PollService poll, Vote vote ) { super( poll, vote, vote.getTime() ); }
365
366
367    }
368
369
370
371   // ====================================================================================
372
373
374    /** An event that records the casting or withdrawal of a vote.
375      */
376    public static class VotingEvent extends ActivityEvent
377    {
378
379        private static final long serialVersionUID = 2L; // see also WithdrawalEvent
380
381
382        private VotingEvent( final PollService poll, final Vote vote, final long time )
383        {
384         // super( time == 0L? System.currentTimeMillis(): time );
385         /// why translate an "unknown" time as current?  instead leave it at "the epoch":
386            super( time );
387
388            pollName = poll.name();
389            candidate = vote.getCandidate();
390            voter = vote.voter();
391        }
392
393
394
395       // --------------------------------------------------------------------------------
396
397
398        /** The candidate for whom the vote was cast, or from whom the vote was withdrawn.
399          */
400        public IDPair candidate() { return candidate; }
401
402
403            private final IDPair candidate;
404
405
406
407        /** Identifies the candidate for whom the vote was cast, or from whom the vote was
408          * withdrawn.  This method is provided for backward compatibility, and will soon
409          * be deprecated.
410          */
411        public String candidateEmail() { return candidate.email(); }
412
413
414
415        /** Names the poll in which the vote was cast or withdrawn.
416          *
417          *     @see Poll#name()
418          */
419        public String pollName() { return pollName; }
420
421
422            private final String pollName;
423
424
425
426        /** The voter.
427          */
428        public IDPair voter() { return voter; }
429
430
431            private final IDPair voter;
432
433
434
435        /** Identifies the voter.  This method is provided for backward compatibility, and
436          * will soon be deprecated.
437          */
438        public String voterEmail() { return voter.email(); }
439
440
441
442    }
443
444
445
446   // ====================================================================================
447
448
449    /** An event that records the withdrawal of a vote.
450      */
451    public static final class WithdrawalEvent extends VotingEvent
452    {
453
454
455        private static final long serialVersionUID = 1L;
456
457
458        /** Creates a WithdrawalEvent.
459          */
460        WithdrawalEvent( final PollService poll, final Vote oldVote, final Vote newVote )
461        {
462            super( poll, oldVote, newVote.getTime() );
463        }
464
465
466    }
467
468
469
470//// P r i v a t e ///////////////////////////////////////////////////////////////////////
471
472
473    private volatile boolean isChanged; // volatile per write()
474
475
476
477}