001package votorola.a; // Copyright 2008, 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.g.lang.*;
006import votorola.g.sql.*;
007
008
009/** The cached geocoding of a residential address, backed by a row of the vote-server's
010  * geocode table.  Caching is employed to lighten the load on the geocoding service.
011  */
012public final class Geocode
013{
014
015
016    /** Constructs a Geocode, reading its initial state from the geocode table, or leaving
017      * it at default values if it does not exist in the table.
018      *
019      *     @param table the vote-server's geocode table.
020      *
021      *     @see #region()
022      *     @see #address()
023      */
024    public Geocode( final String region, final String address, final Table table )
025      throws SQLException
026    {
027        this( region, address );
028        table.get( region, address, Geocode.this );
029    }
030
031
032
033    /** Constructs a Geocode with default initial data.
034      *
035      *     @see #region()
036      *     @see #address()
037      */
038    private Geocode( String _region,  String _address )
039    {
040        if( _region == null || _address == null ) throw new NullPointerException(); // fail fast
041
042        region = _region;
043        address = _address;
044    }
045
046
047
048   // ------------------------------------------------------------------------------------
049
050
051    /** The street address that is geocoded.  It ought to be in minimal canonical form,
052      * such as a normalized postal code, otherwise the geocode table may grow
053      * unnecessarily large.
054      */
055    public String address() { return address; }
056
057
058        private final String address;
059
060
061
062    /** Returns true if this geocode has been stored in the table; false otherwise.
063      */
064    public boolean exists() { return timestamp > 0L; }
065
066
067
068    /** The latitude of the address in radians.
069      *
070      *   @see #setCoordinates(double,double)
071      */
072    public double latitude() { return latitude; };
073
074
075        private double latitude;
076
077
078
079    /** The longitude of the address in radians.
080      *
081      *   @see #setCoordinates(double,double)
082      */
083    public double longitude() { return longitude; };
084
085
086        private double longitude;
087
088
089
090    /** The country in which the address is specified.  This is a two-character, country
091      * code top-level domain (<a
092      * href='http://en.wikipedia.org/wiki/Country_code_top-level_domain' >ccTLD</a>).
093      */
094    public String region() { return region; }
095
096
097        private final String region;
098
099
100
101    /** Sets the latitude and longitude.
102      *
103      *     @see #latitude()
104      *     @see #longitude()
105      */
106    public void setCoordinates( double newLatitude, double newLongitude )
107    {
108        latitude = newLatitude;
109        longitude = newLongitude;
110    }
111
112
113
114    /** The time at which this geocode was last stored to the table, in milliseconds since
115      * the 'epoch'; or zero, if it was never stored.  This is intended to implement a
116      * rolling turn-over of the cache, keeping it refeshed with relatively recent values
117      * (say no older than five years) from the geocoding service - but that has not been
118      * implemented yet.
119      *
120      *     @see System#currentTimeMillis()
121      */
122    public final long timestamp() { return timestamp; }
123
124
125        private long timestamp = 0L;
126
127
128
129    /** Stores this geocode in the table.
130      *
131      *     @param table the vote-server's geocode table.
132      */
133    public void write( final Table table ) throws SQLException
134    {
135        timestamp = System.currentTimeMillis();
136        table.put( Geocode.this );
137    }
138
139
140
141   // - O b j e c t ----------------------------------------------------------------------
142
143
144    /** Returns a descripion of this geocode, including the address and its geographic
145      * coordinates.
146      */
147    public @Override String toString()
148    {
149        return "Geocode[" + address + " (" + latitude + ", " + longitude + ")]";
150    }
151
152
153
154   // ====================================================================================
155
156
157    /** Thrown when a geocoding-specific IO exception occurs.
158      */
159    public static final class GeocodingException extends IOException
160    {
161
162        public GeocodingException( String message ) { super( message ); }
163
164    }
165
166
167
168   // ====================================================================================
169
170
171    /** The geocode table of a vote-server, caching geocode data in relational form.
172      */
173    public static @ThreadSafe final class Table
174    {
175
176
177        /** Constructs a Table, physically creating it if it does not already exist.
178          *
179          *     @see #database()
180          */
181        Table( Database _database ) throws SQLException
182        {
183            database = _database;
184            final String sKey = statementKeyBase + "init";
185            synchronized( database )
186            {
187                PreparedStatement s = database.statementCache().get( sKey );
188                if( s == null )
189                {
190                    s = database.connection().prepareStatement(
191                     "CREATE TABLE IF NOT EXISTS \"" + tableName + "\""
192                      + " (region character varying,"
193                      +  " address character varying,"
194                      +  " latitude double precision NOT NULL,"
195                      +  " longitude double precision NOT NULL,"
196                      +  " timestamp bigint NOT NULL,"
197                      +  " PRIMARY KEY (region, address))" );
198                    database.statementCache().put( sKey, s );
199                }
200                s.execute();
201            }
202        }
203
204
205
206       // --------------------------------------------------------------------------------
207
208
209        /** The database in which this table is stored.
210          */
211        public @Warning("thread restricted object") Database database() { return database; }
212
213
214            private final Database database;
215
216
217
218    //// P r i v a t e ///////////////////////////////////////////////////////////////////
219
220
221        /** Retrieves a geocode from the table, initializing the fields of a Geocode
222          * instance.
223          *
224          *     @param geocode the instance to initialize.  All fields are assumed to be
225          *       at default values.
226          *     @return true if the geocode was retrieved, false if it did not exist in
227          *       the table.
228          */
229        private boolean get( final String region, final String address, Geocode geocode )
230          throws SQLException
231        {
232            final String sKey = statementKeyBase + "get";
233            synchronized( database )
234            {
235                PreparedStatement s = database.statementCache().get( sKey );
236                if( s == null )
237                {
238                    s = database.connection().prepareStatement(
239                      "SELECT latitude,longitude,timestamp"
240                       + " FROM \"" + tableName + "\"" + " WHERE region = ? AND address = ?" );
241                    database.statementCache().put( sKey, s );
242                }
243                s.setString( 1, region );
244                s.setString( 2, address );
245                final ResultSet r = s.executeQuery();
246                try
247                {
248                    if( !r.next() ) return false;
249
250                    geocode.latitude = r.getDouble( 1 );
251                    geocode.longitude = r.getDouble( 2 );
252                    geocode.timestamp = r.getLong( 3 );
253                    return true;
254                }
255                finally{ r.close(); }
256            }
257        }
258
259
260
261        /** Stores a geocode to this table.
262          */
263        private void put( final Geocode geocode ) throws SQLException
264        {
265            synchronized( database )
266            {
267                // effect an "upsert" in PostgreSQL
268                // http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-postgresql/6527838#6527838
269                final Connection c = database.connection();
270                {
271                    final String sKey = statementKeyBase + "putU";
272                    PreparedStatement s = database.statementCache().get( sKey );
273                    if( s == null )
274                    {
275                        s = c.prepareStatement( "UPDATE \"" + tableName + "\""
276                          + " SET latitude = ?, longitude = ?, timestamp = ?"
277                          + " WHERE region = ? AND address = ?" );
278                        database.statementCache().put( sKey, s );
279                    }
280                    s.setDouble( 1, geocode.latitude );
281                    s.setDouble( 2, geocode.longitude );
282                    s.setLong( 3, geocode.timestamp );
283                    s.setString( 4, geocode.region );
284                    s.setString( 5, geocode.address );
285                    final int updatedRows = s.executeUpdate();
286                    if( updatedRows > 0 ) { assert updatedRows == 1; return; }
287                }
288                {
289                    final String sKey = statementKeyBase + "putI";
290                    PreparedStatement s = database.statementCache().get( sKey );
291                    if( s == null )
292                    {
293                        s = c.prepareStatement( "INSERT INTO \"" + tableName + "\""
294                          + " (region, address, latitude, longitude, timestamp)"
295                          + " SELECT ?, ?, ?, ?, ? WHERE NOT EXISTS"
296                          + " (SELECT 1 FROM \"" + tableName + "\""
297                          +   " WHERE region = ? AND address = ?)" );
298                        database.statementCache().put( sKey, s );
299                    }
300                    s.setString( 1, geocode.region );
301                    s.setString( 2, geocode.address );
302                    s.setDouble( 3, geocode.latitude );
303                    s.setDouble( 4, geocode.longitude );
304                    s.setLong( 5, geocode.timestamp );
305                    s.setString( 6, geocode.region );
306                    s.setString( 7, geocode.address );
307                    s.executeUpdate();
308                }
309            }
310        }
311
312
313
314        private static final String statementKeyBase;
315
316
317
318        private static final String tableName = "geocode";
319
320
321            static { statementKeyBase = Table.class.getName() + ":" + tableName + "."; }
322
323
324    }
325
326
327
328}