package 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. import java.io.*; import java.sql.*; import javax.xml.stream.*; import votorola.a.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.sql.*; import static votorola.a.voter.IDPair.NOBODY; /** A voter's input to a poll backed by a row of the input table. * * @see InputTable */ public @ThreadRestricted("touch") final class Vote implements Cloneable, Serializable { private static final long serialVersionUID = 3L; /** Constructs a Vote from an ID pair, reading the initial state from the input table. */ public Vote( IDPair _voter, final VoterInputTable voterInputTable ) throws SQLException { this( _voter ); assert voterInputTable.voterService() instanceof PollService; final String xml = voterInputTable.get( voter.email() ); if( xml == null ) LoggerX.i(getClass()).finest( "stored input is null, assuming defaults: " + voter.email() ); else { try{ init( xml ); } catch( final XMLStreamException x ) { throw voterInputTable.newUnparseableInputException( voter.email(), xml, x ); } } } /** Constructs a Vote from an email address, reading the initial state from the input * table. */ public Vote( final String voterEmail, VoterInputTable _voterInputTable ) throws SQLException { this( IDPair.fromEmail(voterEmail), _voterInputTable ); } /** Constructs a Vote from the specified initial data. * * @param xml the initial data from the '{@linkplain VoterInputTable#get(String) * xml}' column of the input table, or null to use defaults. */ Vote( final IDPair voter, final String xml ) throws XMLStreamException { this( voter ); if( xml != null ) init( xml ); } /** Constructs a Vote from the specified initial data. * * @param xml the initial data from the '{@linkplain VoterInputTable#get(String) * xml}' column of the input table, or null to use defaults. */ public Vote( final String voterEmail, final String xml ) throws XMLStreamException { this( IDPair.fromEmail(voterEmail), xml ); } /** Constructs a Vote with default initial data. */ public Vote( IDPair _voter ) { if( _voter == null ) throw new NullPointerException(); // fail fast voter = _voter; } /** Constructs a Vote with default initial data. */ public Vote( final String voterEmail ) { this( IDPair.fromEmail( voterEmail )); } private void init( final String xml ) throws XMLStreamException { final StringReader rS = new StringReader( xml ); try { final XMLStreamReader r = XMLColumnAppender.newStreamReader( rS ); try { while( r.hasNext() ) { r.next(); if( r.isStartElement() && "X".equals( r.getLocalName() )) { candidate = IDPair.fromEmail( r.getAttributeValue( /*namespaceURI*/null, "c" )); dartSector = XMLColumnAppender.stringToByte( r.getAttributeValue( /*namespaceURI*/null, "dS" )); time = XMLColumnAppender.stringToLong( r.getAttributeValue( /*namespaceURI*/null, "t" )); break; } } } finally{ r.close(); } } finally{ rS.close(); } } // ------------------------------------------------------------------------------------ // Ensure that any field a Wicket form might want to access is provided with an // accessor method that is declared public (package is not suffient). See // votorola.a.web.wic.VPage. /** The candidate for whom the voter is voting, or {@linkplain IDPair#NOBODY NOBODY} * if the voter's vote is currently uncast. * * @see #setCandidate(IDPair) */ public IDPair getCandidate() { return candidate; } private IDPair candidate = NOBODY; /** Changes the candidate for whom the voter is voting, and resets the timestamp. * * @see #getCandidate() * @see #resetTime() */ public void setCandidate( final IDPair newCandidate ) { if( newCandidate == null ) throw new NullPointerException(); // fail fast if( candidate.equals( newCandidate )) return; candidate = newCandidate; isChanged = true; resetTime(); } /** Identifies the candidate for whom the voter is voting. This method is provided * for backward compatibility and will soon be deprecated. * * @return the canonical email address of the candidate, or null if the vote is * currently uncast. */ public String getCandidateEmail() { final IDPair c = candidate; // snapshot copy, for atomic test/return return c.equals(NOBODY)? null: c.email(); } /** Changes the candidate for whom the voter is voting, and resets the timestamp. * This method is provided for backward compatibility, and * will soon be deprecated. */ public void setCandidateEmail( final String newCandidateEmail ) { setCandidate( IDPair.fromEmail( newCandidateEmail )); } /** Returns the dart sector that was last assigned by a count, if any. * * @return a dart sector of 1 to {@value * votorola.a.count.CountNode#DART_SECTOR_MAX}, or zero if no dart * sector has been assigned. * * @see #setDartSector(int) * @see CountNode#dartSector() */ public byte getDartSector() { return dartSector; } private byte dartSector; /** Sets the dart sector. * * @see #getDartSector() */ public void setDartSector( int _newSector ) { final byte newSector = (byte)_newSector; if( newSector == dartSector ) return; dartSector = newSector; isChanged = true; } /** Specifies the time at which the voter last altered this vote. * * @return the time in milliseconds since the Epoch, or 0L if the time is * unknown, or if this vote has never been altered by the voter. * * @see #getCandidateEmail() * @see #setTime(long) */ public long getTime() { return time; } private long time; /** Sets the time at which the voter last altered this vote to the current clock * time. * * @see #getTime() */ public void resetTime() { setTime( System.currentTimeMillis() ); } /** Sets the time at which the voter last altered this vote. * * @see #getTime() */ public void setTime( final long newTime ) { if( newTime == time ) return; time = newTime; isChanged = true; } /** The voter. */ public IDPair voter() { return voter; } private final IDPair voter; /** Identifies the voter. This method is provided for backward compatibility, and * will soon be deprecated. * * @return the canonical email address. * * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public String voterEmail() { return voter.email(); } /** Writes this vote to the table if it has unwritten changes, or deletes the vote if * it's at default. * * @param userSession the session of the user requesting the change, or null if * the change is not user requested. */ void write( VoterInputTable voterInputTable, ServiceSession userSession ) throws SQLException, VoterInputTable.BadInputException { write( voterInputTable, userSession, /*toForce*/false ); } /** Writes this vote to the table, or deletes the vote if it's at default. * * @param toForce false to write only if changes were made; true to force the * write regardless. * @param userSession the session of the user requesting the change, or null if * the change is not user requested. */ public void write( final VoterInputTable voterInputTable, final ServiceSession userSession, final boolean toForce ) throws SQLException, VoterInputTable.BadInputException { if( !( toForce || isChanged )) return; if( voter.equals( NOBODY )) throw new IllegalArgumentException(); // you may create nobody's vote if you don't like to model it as null, but you // may not store it final StringBuilder b = new StringBuilder(); { final VoterInputTable.XMLColumnBuilder bC = new VoterInputTable.XMLColumnBuilder( b ); bC.appendAttribute( "c", getCandidateEmail() ); bC.appendAttribute( "dS", XMLColumnAppender.byteToString(dartSector) ); bC.appendAttribute( "t", XMLColumnAppender.longToString(time) ); } isChanged = false; // early, in case clobbering another thread's true - isChanged must be volatile for this try { if( b.length() == 0 ) { LoggerX.i(getClass()).finest( "deleting storage, all data is at default" ); voterInputTable.delete( voterEmail() ); } else { b.insert( 0, "" ); voterInputTable.put( voterEmail(), b.toString(), userSession ); } } catch( RuntimeException x ) { isChanged = true; throw x; } // rollback catch( SQLException x ) { isChanged = true; throw x; } } // - O b j e c t ---------------------------------------------------------------------- public final @Override Vote clone() { try { return (Vote)super.clone(); } catch( CloneNotSupportedException x ) { throw new RuntimeException( x ); } // never occurs } // ==================================================================================== /** An event that records the casting of a vote. */ public static final class CastEvent extends VotingEvent { /** Constructs a CastEvent. */ CastEvent( PollService poll, Vote vote ) { super( poll, vote, vote.getTime() ); } } // ==================================================================================== /** An event that records the casting or withdrawal of a vote. */ public static class VotingEvent extends ActivityEvent { private static final long serialVersionUID = 2L; // see also WithdrawalEvent private VotingEvent( final PollService poll, final Vote vote, final long time ) { // super( time == 0L? System.currentTimeMillis(): time ); /// why translate an "unknown" time as current? instead leave it at "the epoch": super( time ); pollName = poll.name(); candidate = vote.getCandidate(); voter = vote.voter(); } // -------------------------------------------------------------------------------- /** The candidate for whom the vote was cast, or from whom the vote was withdrawn. */ public IDPair candidate() { return candidate; } private final IDPair candidate; /** Identifies the candidate for whom the vote was cast, or from whom the vote was * withdrawn. This method is provided for backward compatibility, and will soon * be deprecated. */ public String candidateEmail() { return candidate.email(); } /** Names the poll in which the vote was cast or withdrawn. * * @see Poll#name() */ public String pollName() { return pollName; } private final String pollName; /** The voter. */ public IDPair voter() { return voter; } private final IDPair voter; /** Identifies the voter. This method is provided for backward compatibility, and * will soon be deprecated. */ public String voterEmail() { return voter.email(); } } // ==================================================================================== /** An event that records the withdrawal of a vote. */ public static final class WithdrawalEvent extends VotingEvent { private static final long serialVersionUID = 1L; /** Creates a WithdrawalEvent. */ WithdrawalEvent( final PollService poll, final Vote oldVote, final Vote newVote ) { super( poll, oldVote, newVote.getTime() ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private volatile boolean isChanged; // volatile per write() }