001package 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. 002 003import java.io.*; 004import java.sql.*; 005import votorola.a.voter.*; 006import votorola.g.*; 007import votorola.g.lang.*; 008import votorola.g.logging.*; 009import votorola.g.sql.*; 010 011 012/** The input table of a voter service, storing its voter input in a relational form. 013 * Multiple services may share a single physical table. The columnar structure is fixed 014 * by the use of a single 'xml' column. 015 */ 016public @ThreadSafe abstract class VoterInputTable<S extends VoterService> 017{ 018 019 020 /** Partially constructs a VoterInputTable. 021 * 022 * @see #tableName() 023 * @see #voterService() 024 * @see #init() 025 */ 026 protected VoterInputTable( S _voterService, String _tableName ) 027 { 028 voterService = _voterService; 029 tableName = _tableName; 030 if( tableName.length() > Database.MAX_IDENTIFIER_LENGTH ) 031 { 032 throw new IllegalArgumentException( "table name exceeds " 033 + Database.MAX_IDENTIFIER_LENGTH + " characters in length: " + tableName ); 034 } 035 036 database = voterService.vsRun().database(); 037 statementKeyBase = getClass().getName() + ":" + tableName + "."; 038 } 039 040 041 042 /** Finishes constructing a VoterInputTable, physically creating it if it does not 043 * already exist. 044 */ 045 public void init() throws SQLException 046 { 047 final String sKey = statementKeyBase + "init"; 048 synchronized( database ) 049 { 050 PreparedStatement s = database.statementCache().get( sKey ); 051 if( s == null ) 052 { 053 s = database.connection().prepareStatement( 054 "CREATE TABLE IF NOT EXISTS \"" + tableName + "\"" 055 + " (voterEmail character varying PRIMARY KEY," 056 + " xml character varying NOT NULL)" ); 057 database.statementCache().put( sKey, s ); 058 } 059 s.execute(); 060 } 061 } 062 063 064 065 // - V o t e r - I n p u t - T a b l e ------------------------------------------------ 066 067 068 /** The database in which this table is stored. 069 * 070 * @see VoteServer.Run#database() 071 */ 072 public @Warning("thread restricted object") final Database database() { return database; } 073 074 075 protected final Database database; 076 077 078 079 /** Removes a voter's data from the table if any is stored there. 080 */ 081 public void delete( final String voterEmail ) throws SQLException 082 { 083 final String sKey = statementKeyBase + "delete"; 084 synchronized( database ) 085 { 086 PreparedStatement s = database.statementCache().get( sKey ); 087 if( s == null ) 088 { 089 s = database.connection().prepareStatement( 090 "DELETE FROM \"" + tableName + "\" WHERE voterEmail = ?" ); 091 database.statementCache().put( sKey, s ); 092 } 093 s.setString( 1, voterEmail ); 094 s.executeUpdate(); 095 } 096 } 097 098 099 100 /** Retrieves a voter's data from the 'xml' column. 101 * 102 * @return the data, or null if the table has no such voter. 103 */ 104 public String get( final String voterEmail ) throws SQLException 105 { 106 final String sKey = statementKeyBase + "get"; 107 synchronized( database ) 108 { 109 PreparedStatement s = database.statementCache().get( sKey ); 110 if( s == null ) 111 { 112 s = database.connection().prepareStatement( 113 "SELECT xml FROM \"" + tableName + "\"" 114 + " WHERE voterEmail = ?" ); 115 database.statementCache().put( sKey, s ); 116 } 117 s.setString( 1, voterEmail ); 118 final ResultSet r = s.executeQuery(); 119 try 120 { 121 if( !r.next() ) return null; 122 123 return r.getString( 1 ); 124 } 125 finally{ r.close(); } 126 } 127 } 128 129 130 131 /** Throws a BadInputException if the input string is longer than {@linkplain 132 * #MAX_INPUT_LENGTH MAX_INPUT_LENGTH}; otherwise returns the same input string. 133 */ 134 public static String lengthConstrained( String inputString ) throws BadInputException 135 { 136 if( inputString != null && inputString.length() > MAX_INPUT_LENGTH ) 137 { 138 throw new BadInputException( "exceeds " + MAX_INPUT_LENGTH + " characters: '" 139 + inputString.substring(0,16) + "...'" ); 140 } 141 142 return inputString; 143 } 144 145 146 147 /** Maximum length of a single input string, in characters. This limit is imposed in 148 * order to guard against attacks centered on the input of large strings. 149 */ 150 public static final int MAX_INPUT_LENGTH = 200; 151 152 153 154 /** Constructs an exception that complains about "unparseable data from input 155 * table...". 156 */ 157 public final VotorolaRuntimeException newUnparseableInputException( String voterEmail, 158 String xml, javax.xml.stream.XMLStreamException nestedException ) 159 { 160 return new VotorolaRuntimeException( 161 "unparseable data from input table for voterEmail = " + voterEmail + 162 ", table name = " + tableName() + ": " + xml, nestedException ); 163 } 164 165 166 167 /** Stores a voter's data to the 'xml' column. 168 * 169 * @param xml the data to store. 170 * @param userSession the session of the user requesting the change, or null if 171 * the change is not user requested. 172 * 173 * @throws VotorolaSecurityException if userSession is specified, and voterEmail 174 * is unequal to userSession.userEmail(). This is a failsafe bug trap. 175 */ 176 public void put( final String voterEmail, final String xml, final ServiceSession userSession ) 177 throws BadInputException, SQLException 178 { 179 if( userSession != null ) testAccessAllowed( voterEmail, userSession ); 180 lengthConstrained( voterEmail ); 181 LoggerX.i(getClass()).finer( "voter input table " + tableName + ", storing for voter " + voterEmail + " : " + xml ); 182 synchronized( database ) 183 { 184 // effect an "upsert" in PostgreSQL 185 // http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-postgresql/6527838#6527838 186 final Connection c = database.connection(); 187 { 188 final String sKey = statementKeyBase + "putU"; 189 PreparedStatement s = database.statementCache().get( sKey ); 190 if( s == null ) 191 { 192 s = c.prepareStatement( "UPDATE \"" + tableName + "\"" 193 + " SET xml = ? WHERE voterEmail = ?" ); 194 database.statementCache().put( sKey, s ); 195 } 196 s.setString( 1, xml ); 197 s.setString( 2, voterEmail); 198 final int updatedRows = s.executeUpdate(); 199 if( updatedRows > 0 ) { assert updatedRows == 1; return; } 200 } 201 { 202 final String sKey = statementKeyBase + "putI"; 203 PreparedStatement s = database.statementCache().get( sKey ); 204 if( s == null ) 205 { 206 s = c.prepareStatement( "INSERT INTO \"" + tableName + "\"" 207 + " (voterEmail, xml) SELECT ?, ? WHERE NOT EXISTS" 208 + " (SELECT 1 FROM \"" + tableName + "\" WHERE voterEmail = ?)" ); 209 database.statementCache().put( sKey, s ); 210 } 211 s.setString( 1, voterEmail); 212 s.setString( 2, xml ); 213 s.setString( 3, voterEmail); 214 s.executeUpdate(); 215 } 216 } 217 } 218 219 220 221 /** The name of this table (relation). It conventionally begins with the prefix 222 * "in_", as for example "in_vote". 223 */ 224 public final String tableName() { return tableName; } 225 226 227 protected final String tableName; 228 229 230 231 /** Throws a VotorolaSecurityException if voterEmail is unequal to 232 * userSession.userEmail(). 233 */ 234 public static void testAccessAllowed( final String voterEmail, final ServiceSession userSession ) 235 throws VotorolaSecurityException 236 { 237 final String userEmail = userSession.userOrNobody().email(); 238 if( !voterEmail.equals( userEmail )) 239 { 240 throw new VotorolaSecurityException( "attempt by user " + userEmail 241 + " to modify input data of voter " + voterEmail ); 242 } 243 } 244 245 246 247 /** Returns the service whose voter input this table stores. 248 */ 249 public S voterService() { return voterService; } 250 251 252 protected final S voterService; 253 254 255 256 // ==================================================================================== 257 258 259 /** Thrown when voter input is unacceptable for storage. 260 */ 261 public static final class BadInputException extends IOException 262 { 263 264 public BadInputException( String message ) { super( message ); } 265 266 } 267 268 269 270 // ==================================================================================== 271 272 273 /** An XML column appender for a voter input table. 274 */ 275 public static class XMLColumnAppender extends votorola.g.sql.XMLColumnAppender 276 { 277 278 /** Constructs an XMLColumnAppender. 279 * 280 * @see #sink() 281 */ 282 public XMLColumnAppender( Appendable _sink ) { super( _sink ); } 283 284 285 /** @throws BadInputException if the attribute value is longer than the allowed 286 * limit. 287 */ 288 public @Override void appendAttribute( final String name, final String value ) 289 throws IOException 290 { 291 lengthConstrained( value ); 292 super.appendAttribute( name, value ); 293 } 294 295 } 296 297 298 299 // ==================================================================================== 300 301 302 /** An XML column appender for a voter input table that sinks to a string builder. 303 * Therefore it throws no IO exceptions, nor is it thread safe. 304 */ 305 public static final class XMLColumnBuilder extends XMLColumnAppender 306 { 307 308 /** Constructs an XMLColumnBuilder. 309 * 310 * @see #sink() 311 */ 312 public XMLColumnBuilder( StringBuilder _sink ) { super( _sink ); } 313 314 315 public @Override void appendAttribute( final String name, final String value ) 316 throws BadInputException 317 { 318 // cf. votorola.g.sql.XMLColumnBuilder 319 try{ super.appendAttribute( name, value ); } 320 catch( BadInputException x ) { throw x; } 321 catch( IOException x ) { throw new IllegalStateException( x ); } // impossible 322 } 323 324 } 325 326 327 328//// P r i v a t e /////////////////////////////////////////////////////////////////////// 329 330 331 protected final String statementKeyBase; 332 333 334}