package votorola.a.register; // Copyright 2008, 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.xml.stream.*;
import votorola._.*;
import votorola.a.*;
import votorola.g.lang.*;
import votorola.g.logging.*;
import votorola.g.sql.*;
import votorola.g.util.*;
/** A committable instance of a voter's input to a register. It is writeable, and its
* changes may be committed to the voter input table.
*/
public @ThreadRestricted("touch") final class RegistrationC implements Cloneable, Registration
{
// Cf. votorola.a.election.Vote
//
// When super registers are supported, the assumed association of one table
// per register, here, will need to be disentangled.
private static final long serialVersionUID = 2L;
/** Constructs a RegistrationC, reading its initial state from
* the voter input table.
*
* @param voterEmail per {@linkplain #voterEmail() voterEmail}()
* @param voterInputTable of the register
*/
public RegistrationC( final String voterEmail, final VoterInputTable voterInputTable )
throws SQLException
{
this( voterEmail );
assert voterInputTable.electoralService() instanceof Register;
final String xml = voterInputTable.get( voterEmail );
if( xml == null ) LoggerX.i(getClass()).finest( "stored input is null, assuming defaults: " + voterEmail );
else
{
try
{
init( xml );
}
catch( XMLStreamException x ) { throw voterInputTable.newUnparseableInputException( voterEmail, xml, x ); }
}
}
/** Constructs a RegistrationC from the specified initial data.
*
* @param voterEmail per {@linkplain #voterEmail() voterEmail}()
* @param xml the initial data from the '{@linkplain VoterInputTable#get(String) xml}'
* column of the voter input table, or null to use defaults
*/
public RegistrationC( String voterEmail, String xml ) throws XMLStreamException
{
this( voterEmail );
if( xml != null ) init( xml );
}
/** Constructs a RegistrationC with default initial data.
*
* @param voterEmail per {@linkplain #voterEmail() voterEmail}()
*/
public RegistrationC( String voterEmail )
{
if( voterEmail == null ) throw new NullPointerException();
this.voterEmail = voterEmail;
}
private final void init( final String xml ) throws XMLStreamException // not dynamically bound when called from constructor/initializer
{
final StringReader sR = new StringReader( xml );
try
{
final XMLStreamReader xmlR = VoterInputTable.createXMLStreamReader( sR );
try
{
while( xmlR.hasNext() )
{
xmlR.next();
if( xmlR.isStartElement() && "X".equals( xmlR.getLocalName() ))
{
link = xmlR.getAttributeValue( /*namespaceURI*/null, "l" );
name = xmlR.getAttributeValue( /*namespaceURI*/null, "n" );
note = xmlR.getAttributeValue( /*namespaceURI*/null, "no" );
residence = xmlR.getAttributeValue( /*namespaceURI*/null, "r" );
trustDestinations.setBackingArray( ArrayX.stringToStrings(
xmlR.getAttributeValue( /*namespaceURI*/null, "t" )));
writeableAll = VoterInputTable.stringToBoolean(
xmlR.getAttributeValue( /*namespaceURI*/null, "writeableAll" ));
break;
}
}
}
finally{ xmlR.close(); }
}
finally{ sR.close(); }
}
// ------------------------------------------------------------------------------------
/** Writes this registration to the table, if it has uncommitted changes;
* or removes it, if it is at default.
*
* @param voterInputTable of the register
*/
public void commit( final VoterInputTable voterInputTable,
ElectoralSubserver.UserSession session )
throws SQLException, VoterInputTable.BadInputException
{
commit( voterInputTable, session, /*toForce*/false );
}
/** Writes this registration to the table; or removes it, if it is at default.
*
* @param voterInputTable of the register
* @param toForce false to commit only if changes were made in this instance
* of registration; true to force the commit, regardless
*/
void commit( final VoterInputTable voterInputTable,
final ElectoralSubserver.UserSession session, final boolean toForce )
throws SQLException, VoterInputTable.BadInputException
{
if( !( toForce || isChanged )) return;
final StringBuilder xmlB = new StringBuilder();
VoterInputTable.appendField( "l", link, xmlB );
VoterInputTable.appendField( "n", name, xmlB );
VoterInputTable.appendField( "no", note, xmlB );
VoterInputTable.appendField( "r", residence, xmlB );
VoterInputTable.appendField( "t",
ArrayX.stringsToString(trustDestinations.getBackingArray()), xmlB );
isChanged = false; // early, in case clobbering another thread's true - isChanged must be volatile for this
try
{
if( xmlB.length() == 0 && writeableAll == WRITEABLE_ALL_DEFAULT ) // once written by registrant, row is permanent (preserving non-default state of writeableAll)
{
LoggerX.i(getClass()).finest( "removing storage, all data is at default" );
voterInputTable.remove( voterEmail );
}
else
{
VoterInputTable.appendField( "writeableAll",
VoterInputTable.booleanToString(writeableAll), xmlB );
xmlB.insert( 0, "" );
voterInputTable.put( voterEmail, xmlB.toString(), session,
/*toBypassSecurityFailsafe*/writeableAll );
}
}
catch( RuntimeException x ) { isChanged = true; throw x; } // rollback
catch( SQLException x ) { isChanged = true; throw x; }
}
// - O b j e c t ----------------------------------------------------------------------
public final @Override RegistrationC clone()
{
try
{
return (RegistrationC)super.clone();
}
catch( CloneNotSupportedException x ) { throw new RuntimeException( x ); } // never occurs
}
// - R e g i s t r a t i o n ----------------------------------------------------------
/** @see #setLink(String,ElectoralSubserver.UserSession,Register)
*/
public String getLink() { return link; } // must be public, or Wicket 1.3.2 will directly access private field
private String link;
/** Changes the link.
*
* @throws VotorolaSecurityException if {@linkplain
* #isWriteable(ElectoralSubserver.UserSession,Register) write permission} denied
* @see #getLink()
*/
public void setLink( String newLink, // must be public, ditto
ElectoralSubserver.UserSession session, Register register )
{
setWriteableAll( session, register );
if( ObjectX.nullEquals( newLink, link )) return;
link = newLink;
isChanged = true;
}
/** @see #setName(String,ElectoralSubserver.UserSession,Register)
*/
public String getName() { return name; } // must be public, ditto
private String name;
/** Changes the name.
*
* @throws VotorolaSecurityException if {@linkplain
* #isWriteable(ElectoralSubserver.UserSession,Register) write permission} denied
* @see #getName()
*/
public void setName( String newName, // must be public, ditto
ElectoralSubserver.UserSession session, Register register )
{
setWriteableAll( session, register );
if( ObjectX.nullEquals( newName, name )) return;
name = newName;
isChanged = true;
}
/** @see #setNote(String,ElectoralSubserver.UserSession,Register)
*/
public String getNote() { return note; } // must be public, ditto
private String note;
/** Changes the note.
*
* @throws VotorolaSecurityException if {@linkplain
* #isWriteable(ElectoralSubserver.UserSession,Register) write permission} denied
* @see #getNote()
*/
public void setNote( String newNote, // must be public, ditto
ElectoralSubserver.UserSession session, Register register )
{
setWriteableAll( session, register );
if( ObjectX.nullEquals( newNote, note )) return;
note = newNote;
isChanged = true;
}
/** @see #setResidence(String,ElectoralSubserver.UserSession,Register)
*/
public String getResidence() { return residence; } // must be public, ditto
private String residence;
/** Changes the residential address.
*
* @throws VotorolaSecurityException if {@linkplain
* #isWriteable(ElectoralSubserver.UserSession,Register) write permission} denied
* @see #getResidence()
*/
public void setResidence( String newResidence, // must be public, ditto
ElectoralSubserver.UserSession session, Register register )
{
setWriteableAll( session, register );
if( ObjectX.nullEquals( newResidence, residence )) return;
residence = newResidence;
isChanged = true;
}
/** Returns true if this registration is writeable by the user.
*
* @see #isWriteableAll()
*/
boolean isWriteable( ElectoralSubserver.UserSession session, Register register )
{
return voterEmail.equals( session.userEmail() )
|| writeableAll && session.userEmail() != null
&& session.userTrustLevel() >= register.preRegistrationTrustLevel();
}
public boolean isWriteableAll() { return writeableAll; }
private boolean writeableAll = WRITEABLE_ALL_DEFAULT;
/** @throws VotorolaSecurityException if write permission denied
*/
private void setWriteableAll( ElectoralSubserver.UserSession session, Register register )
{
if( !isWriteable( session, register ))
{
throw new VotorolaSecurityException(
"write permission denied, registrant=" + voterEmail
+ ", writer=" + session.userEmail() );
}
if( writeableAll && voterEmail.equals( session.userEmail() ))
{
writeableAll = false;
isChanged = true;
}
}
/** @see #addTrustDestination(String,ElectoralSubserver.UserSession)
* @see #removeTrustDestination(String,ElectoralSubserver.UserSession)
*/
public List trustDestinations() { return trustDestinations; }
private ArrayListU.Open trustDestinations =
new ArrayListU.Open( new String[0] );
/** Adds a new trust destination, if not already present.
*
* @param newDestinationEmail identifying the destination to add
*
* @throws IllegalStateException if addition would exceed TRUST_DESTINATIONS_MAX
* @throws VotorolaSecurityException if voterEmail is unequal
* to session.userEmail() (failsafe bug trap)
* @see #trustDestinations()
*/
public void addTrustDestination( String newDestinationEmail,
ElectoralSubserver.UserSession session )
{
if( newDestinationEmail == null ) throw new NullPointerException(); // fail fast
if( trustDestinations.size() >= TRUST_DESTINATIONS_MAX ) throw new IllegalStateException( "attempt to exceed TRUST_DESTINATIONS_MAX" );
VoterInputTable.testAccessAllowed( voterEmail, session );
String[] trustDestinationArray = trustDestinations.getBackingArray();
for( String destinationEmail: trustDestinationArray )
{
if( destinationEmail.equals( newDestinationEmail )) return; // already trusted
}
trustDestinationArray = Arrays.copyOf
( trustDestinationArray, trustDestinationArray.length + 1 );
trustDestinationArray[trustDestinationArray.length - 1] = newDestinationEmail;
trustDestinations.setBackingArray( trustDestinationArray );
isChanged = true;
}
/** Removes a trust destination, if found.
*
* @param oldDestinationEmail identifying the destination to remove
*
* @throws VotorolaSecurityException if voterEmail is unequal
* to session.userEmail() (failsafe bug trap)
* @see #trustDestinations()
*/
public void removeTrustDestination( String oldDestinationEmail,
ElectoralSubserver.UserSession session )
{
VoterInputTable.testAccessAllowed( voterEmail, session );
String[] oldArray = trustDestinations.getBackingArray();
int r = 0;
for( ;; ++r )
{
if( r >= oldArray.length ) return;
if( oldArray[r].equals( oldDestinationEmail )) break;
}
String[] newArray = new String[oldArray.length - 1];
for( int o = 0, n = 0; o < oldArray.length; ++o )
{
if( o == r ) continue; // the removed one
newArray[n] = oldArray[o];
++n;
}
trustDestinations.setBackingArray( newArray );
isChanged = true;
}
public String voterEmail() { return voterEmail; }
private final String voterEmail;
//// P r i v a t e ///////////////////////////////////////////////////////////////////////
private volatile boolean isChanged; // volatile per commit()
}