package votorola.a; // Copyright 2007-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 javax.xml.stream.*; import votorola._.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.script.*; import votorola.g.sql.*; /** The voter input table of an electoral service, storing voter input in relational form. */ public final @ThreadSafe class VoterInputTable { /** Constructs a VoterInputTable, physical creating it if it does not already exist. * * @param d database per {@linkplain #database() database}() * @param eS electoralService per {@linkplain #electoralService() electoralService}() */ public VoterInputTable( ElectoralService eS, Database d ) throws MisconfigurationException, SQLException { electoralService = eS; database = d; if( tableName().length() > Database.MAX_IDENTIFIER_LENGTH ) { throw new MisconfigurationException( "table name (based on service name) exceeds " + Database.MAX_IDENTIFIER_LENGTH + " characters in length: " + tableName(), electoralService.configurationFile() ); } statementKeyBase = getClass().getName() + ":" + tableName() + "."; if( !exists() ) { final String key = statementKeyBase + "init"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "CREATE TABLE \"" + tableName() + "\"" + " (voterEmail character varying PRIMARY KEY," + " xml character varying NOT NULL)" ); database.statementCache().put( key, s ); } s.execute(); } } } // ------------------------------------------------------------------------------------ /** Serializes the string value of a field and appends it * to a string builder, destined for writing to the 'xml' column. * If the value is null, does nothing; otherwise, appends a serial form * consisting of a space delimiter, followed by an attribute declaration. * * @param nameA field's archival name, a valid XML name; * to use as the attribute name * @param value whose {@linkplain Object#toString() toString}() * is escaped where necessary with XML character entities, * and used as the attribute value; or null to append nothing * * @throws BadInputException if the field value is longer * than the allowed limit */ public static void appendField( final String nameA, final String value, final StringBuilder xmlB ) throws BadInputException { if( value == null ) return; if( value.length() == 0 ) { assert false; return; } lengthConstrained( value ); xmlB.append( ' ' ).append( nameA ).append( '=' ); final char quote; final String quoteEscaped; if( value.contains( "'" )) { quote = '"'; quoteEscaped = """; } else { quote = '\''; quoteEscaped = "'"; // although never used, as value does not contain "'" } xmlB.append( quote ); for( int c = 0, cN = value.length(); c < cN; ++c ) { char ch = value.charAt( c ); if( ch == quote ) xmlB.append( quoteEscaped ); else if( ch == '\t' ) xmlB.append( " " ); else if( ch == '\n' ) xmlB.append( " " ); else if( ch == '\r' ) xmlB.append( " " ); else if( ch == '&' ) xmlB.append( "&" ); else if( ch == '<' ) xmlB.append( "<" ); else xmlB.append( ch ); } xmlB.append( quote ); } /** Returns a non-empty string if b is true; null otherwise. * * @see #stringToBoolean(String) */ public static String booleanToString( boolean b ) { if( b ) return "t"; else return null; } /** Returns true if the string is non-null and non-empty; * false otherwise. * * @see #booleanToString(boolean) */ public static boolean stringToBoolean( String s ) { return s != null && s.length() > 0; } /** Returns true if the specified data string contains a serialization * delimiter character (tab or newline), making it invalid for storage * in a voter input table. */ public static boolean containsDelimiter( final String dataString ) { for( int c = dataString.length() - 1; c >= 0; --c ) { char ch = dataString.charAt( c ); if( ch == '\t' || ch == '\n' ) return true; } return false; } /** Constructs a new XMLStreamReader that is configured to read the data * of the 'xml' column. * * @see XMLInputFactory#createXMLStreamReader(Reader) */ public static synchronized XMLStreamReader createXMLStreamReader( Reader reader ) throws XMLStreamException { return xmlInputFactory.createXMLStreamReader( reader ); } @ThreadRestricted("VoterInputTable.class") // thread safety of this object is undocumented private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); static { xmlInputFactory.setProperty( "javax.xml.stream.isNamespaceAware", Boolean.FALSE ); xmlInputFactory.setProperty( "javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE ); } /** Returns the database in which this table is stored -- * a reference to a shared instance (not thread safe). * Although this method is thread safe, the object it returns is not; * it has its own thread-saftety restrictions, q.v. */ public Database database() { return database; } private final Database database; /** Returns the electoral service whose voter input this table stores. */ public ElectoralService electoralService() { return electoralService; } private final ElectoralService electoralService; /** Retrieves a voter's data from the 'xml' column. * * @return data, or null if the table has no such voter */ public String get( final String voterEmail ) throws SQLException { final String key = statementKeyBase + "get"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT xml FROM \"" + tableName() + "\"" + " WHERE voterEmail = ?" ); database.statementCache().put( key, 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. */ public static final int MAX_INPUT_LENGTH = 200; /** Constructs an exception that complains * about "unparseable data from input table...". */ public VotorolaRuntimeException newUnparseableInputException( String voterEmail, String xml, XMLStreamException nestedException ) { return new VotorolaRuntimeException( "unparseable data from input table for voterEmail " + voterEmail + ", name " + electoralService.name() + ": " + xml, nestedException ); } /** Stores a voter's data to the 'xml' column. * * @param xml data to store * * @throws VotorolaSecurityException if voterEmail is unequal * to userSession.userEmail() (failsafe bug trap) */ public void put( String voterEmail, String xml, ElectoralSubserver.UserSession userSession ) throws BadInputException, SQLException { put( voterEmail, xml, userSession, /*toBypassSecurityFailsafe*/false ); } /** Stores a voter's data to the 'xml' column. * * @param xml data to store * * @throws VotorolaSecurityException if toBypassSecurityFailsafe is false, * and voterEmail is unequal to userSession.userEmail() (failsafe bug trap) */ public void put( final String voterEmail, final String xml, final ElectoralSubserver.UserSession userSession, final boolean toBypassSecurityFailsafe ) throws BadInputException, SQLException { if( containsDelimiter( voterEmail )) throw new VotorolaRuntimeException( "invalid voter email address: " + voterEmail ); if( !toBypassSecurityFailsafe ) testAccessAllowed( voterEmail, userSession ); LoggerX.i(getClass()).finer( "service " + electoralService.name() + ", storing input for voter " + voterEmail + " : " + xml ); assert !containsDelimiter( xml ); synchronized( database ) { { final String key = statementKeyBase + "putU"; PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "UPDATE \"" + tableName() + "\"" + " SET xml = ? WHERE voterEmail = ?" ); database.statementCache().put( key, s ); } s.setString( 1, xml ); s.setString( 2, lengthConstrained( voterEmail )); final int updatedRows = s.executeUpdate(); if( updatedRows > 0 ) { assert updatedRows == 1; return; } } { final String key = statementKeyBase + "putI"; PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "INSERT INTO \"" + tableName() + "\"" + " (voterEmail, xml) VALUES (?, ?)" ); database.statementCache().put( key, s ); } s.setString( 1, lengthConstrained( voterEmail )); s.setString( 2, xml ); s.executeUpdate(); } } } /** Removes a voter's data from the table, if any is stored there. */ public void remove( final String voterEmail ) throws SQLException { final String key = statementKeyBase + "remove"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "DELETE FROM \"" + tableName() + "\" WHERE voterEmail = ?" ); database.statementCache().put( key, s ); } s.setString( 1, voterEmail ); s.executeUpdate(); } } /** Throws a VotorolaSecurityException if voterEmail is unequal * to userSession.userEmail(). */ public static void testAccessAllowed( String voterEmail, ElectoralSubserver.UserSession userSession ) throws VotorolaSecurityException { if( !voterEmail.equals( userSession.userEmail() )) { throw new VotorolaSecurityException( "attempt by user " + userSession.userEmail() + " to modify input data of voter " + voterEmail ); } } // ==================================================================================== /** Thrown when voter input is unacceptable for storage. */ public static final class BadInputException extends VotorolaException { // public BadInputException() {} // public BadInputException( Throwable cause ) { super( cause ); } public BadInputException( String message ) { super( message ); } // public BadInputException( String message, Throwable cause ) { super( message, cause ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Returns true if this table exists; false otherwise. */ private boolean exists() throws SQLException { final String key = statementKeyBase + "exists"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT * FROM \"" + tableName() + "\"" ); s.setMaxRows( 1 ); database.statementCache().put( key, s ); } try { s.execute(); } catch( SQLException x ) { if( "42P01".equals( x.getSQLState() )) return false; // UNDEFINED TABLE throw x; } } return true; } private final String statementKeyBase; private String tableName() { return electoralService.name(); } }