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}