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}