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}