package votorola.a; // Copyright 2007-2009, 2011-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 votorola.a.voter.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.sql.*; /** The input table of a voter service, storing its voter input in a relational form. * Multiple services may share a single physical table. The columnar structure is fixed * by the use of a single 'xml' column. */ public @ThreadSafe abstract class VoterInputTable { /** Partially constructs a VoterInputTable. * * @see #tableName() * @see #voterService() * @see #init() */ protected VoterInputTable( S _voterService, String _tableName ) { voterService = _voterService; tableName = _tableName; if( tableName.length() > Database.MAX_IDENTIFIER_LENGTH ) { throw new IllegalArgumentException( "table name exceeds " + Database.MAX_IDENTIFIER_LENGTH + " characters in length: " + tableName ); } database = voterService.vsRun().database(); statementKeyBase = getClass().getName() + ":" + tableName + "."; } /** Finishes constructing a VoterInputTable, physically creating it if it does not * already exist. */ public void init() throws SQLException { final String sKey = statementKeyBase + "init"; synchronized( database ) { PreparedStatement s = database.statementCache().get( sKey ); if( s == null ) { s = database.connection().prepareStatement( "CREATE TABLE IF NOT EXISTS \"" + tableName + "\"" + " (voterEmail character varying PRIMARY KEY," + " xml character varying NOT NULL)" ); database.statementCache().put( sKey, s ); } s.execute(); } } // - V o t e r - I n p u t - T a b l e ------------------------------------------------ /** The database in which this table is stored. * * @see VoteServer.Run#database() */ public @Warning("thread restricted object") final Database database() { return database; } protected final Database database; /** Removes a voter's data from the table if any is stored there. */ public void delete( final String voterEmail ) throws SQLException { final String sKey = statementKeyBase + "delete"; synchronized( database ) { PreparedStatement s = database.statementCache().get( sKey ); if( s == null ) { s = database.connection().prepareStatement( "DELETE FROM \"" + tableName + "\" WHERE voterEmail = ?" ); database.statementCache().put( sKey, s ); } s.setString( 1, voterEmail ); s.executeUpdate(); } } /** Retrieves a voter's data from the 'xml' column. * * @return the data, or null if the table has no such voter. */ public String get( final String voterEmail ) throws SQLException { final String sKey = statementKeyBase + "get"; synchronized( database ) { PreparedStatement s = database.statementCache().get( sKey ); if( s == null ) { s = database.connection().prepareStatement( "SELECT xml FROM \"" + tableName + "\"" + " WHERE voterEmail = ?" ); database.statementCache().put( sKey, s ); } s.setString( 1, voterEmail ); final ResultSet r = s.executeQuery(); try { if( !r.next() ) return null; return r.getString( 1 ); } finally{ r.close(); } } } /** Throws a BadInputException if the input string is longer than {@linkplain * #MAX_INPUT_LENGTH MAX_INPUT_LENGTH}; otherwise returns the same input string. */ public static String lengthConstrained( String inputString ) throws BadInputException { if( inputString != null && inputString.length() > MAX_INPUT_LENGTH ) { throw new BadInputException( "exceeds " + MAX_INPUT_LENGTH + " characters: '" + inputString.substring(0,16) + "...'" ); } return inputString; } /** Maximum length of a single input string, in characters. This limit is imposed in * order to guard against attacks centered on the input of large strings. */ public static final int MAX_INPUT_LENGTH = 200; /** Constructs an exception that complains about "unparseable data from input * table...". */ public final VotorolaRuntimeException newUnparseableInputException( String voterEmail, String xml, javax.xml.stream.XMLStreamException nestedException ) { return new VotorolaRuntimeException( "unparseable data from input table for voterEmail = " + voterEmail + ", table name = " + tableName() + ": " + xml, nestedException ); } /** Stores a voter's data to the 'xml' column. * * @param xml the data to store. * @param userSession the session of the user requesting the change, or null if * the change is not user requested. * * @throws VotorolaSecurityException if userSession is specified, and voterEmail * is unequal to userSession.userEmail(). This is a failsafe bug trap. */ public void put( final String voterEmail, final String xml, final ServiceSession userSession ) throws BadInputException, SQLException { if( userSession != null ) testAccessAllowed( voterEmail, userSession ); lengthConstrained( voterEmail ); LoggerX.i(getClass()).finer( "voter input table " + tableName + ", storing for voter " + voterEmail + " : " + xml ); synchronized( database ) { // effect an "upsert" in PostgreSQL // http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-postgresql/6527838#6527838 final Connection c = database.connection(); { final String sKey = statementKeyBase + "putU"; PreparedStatement s = database.statementCache().get( sKey ); if( s == null ) { s = c.prepareStatement( "UPDATE \"" + tableName + "\"" + " SET xml = ? WHERE voterEmail = ?" ); database.statementCache().put( sKey, s ); } s.setString( 1, xml ); s.setString( 2, voterEmail); final int updatedRows = s.executeUpdate(); if( updatedRows > 0 ) { assert updatedRows == 1; return; } } { final String sKey = statementKeyBase + "putI"; PreparedStatement s = database.statementCache().get( sKey ); if( s == null ) { s = c.prepareStatement( "INSERT INTO \"" + tableName + "\"" + " (voterEmail, xml) SELECT ?, ? WHERE NOT EXISTS" + " (SELECT 1 FROM \"" + tableName + "\" WHERE voterEmail = ?)" ); database.statementCache().put( sKey, s ); } s.setString( 1, voterEmail); s.setString( 2, xml ); s.setString( 3, voterEmail); s.executeUpdate(); } } } /** The name of this table (relation). It conventionally begins with the prefix * "in_", as for example "in_vote". */ public final String tableName() { return tableName; } protected final String tableName; /** Throws a VotorolaSecurityException if voterEmail is unequal to * userSession.userEmail(). */ public static void testAccessAllowed( final String voterEmail, final ServiceSession userSession ) throws VotorolaSecurityException { final String userEmail = userSession.userOrNobody().email(); if( !voterEmail.equals( userEmail )) { throw new VotorolaSecurityException( "attempt by user " + userEmail + " to modify input data of voter " + voterEmail ); } } /** Returns the service whose voter input this table stores. */ public S voterService() { return voterService; } protected final S voterService; // ==================================================================================== /** Thrown when voter input is unacceptable for storage. */ public static final class BadInputException extends IOException { public BadInputException( String message ) { super( message ); } } // ==================================================================================== /** An XML column appender for a voter input table. */ public static class XMLColumnAppender extends votorola.g.sql.XMLColumnAppender { /** Constructs an XMLColumnAppender. * * @see #sink() */ public XMLColumnAppender( Appendable _sink ) { super( _sink ); } /** @throws BadInputException if the attribute value is longer than the allowed * limit. */ public @Override void appendAttribute( final String name, final String value ) throws IOException { lengthConstrained( value ); super.appendAttribute( name, value ); } } // ==================================================================================== /** An XML column appender for a voter input table that sinks to a string builder. * Therefore it throws no IO exceptions, nor is it thread safe. */ public static final class XMLColumnBuilder extends XMLColumnAppender { /** Constructs an XMLColumnBuilder. * * @see #sink() */ public XMLColumnBuilder( StringBuilder _sink ) { super( _sink ); } public @Override void appendAttribute( final String name, final String value ) throws BadInputException { // cf. votorola.g.sql.XMLColumnBuilder try{ super.appendAttribute( name, value ); } catch( BadInputException x ) { throw x; } catch( IOException x ) { throw new IllegalStateException( x ); } // impossible } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// protected final String statementKeyBase; }