package votorola.a; // Copyright 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 votorola.g.lang.*; import votorola.g.sql.*; /** The cached geocoding of a residential address, backed by a row of the subserver's * * geocode table. Caching is employed to lighten the load on the geocoding service. */ public final class Geocode { /** Constructs a Geocode, reading its initial state from the geocode table, * or leaving it at default values if it does not exist in the table. * * @param address per {@linkplain #address() address}() * @param table the subserver's geocode table * * @see #exists() */ public Geocode( final String address, final Table table ) throws SQLException { this( address ); table.get( address, Geocode.this ); } /** Constructs a Geocode with default initial data. * * @param address per {@linkplain #address() address}() */ private Geocode( String address ) { if( address == null ) throw new NullPointerException(); // fail fast this.address = address; } // ------------------------------------------------------------------------------------ /** The residential address that is geocoded. The exact format depends on the * geocoding service. */ public String address() { return address; } private final String address; /** Stores this geocode in the table. * * @param table the subserver's geocode table */ public void commit( final Table table ) throws SQLException { timestamp = System.currentTimeMillis(); table.put( Geocode.this ); } /** Returns true if this geocode has been stored in the table; false otherwise. */ public boolean exists() { return timestamp > 0L; } /** The latitude of the address, in radians. * * @see #setCoordinates(Double,Double) */ public double latitude() { return latitude; }; private double latitude; /** The longitude of the address, in radians. * * @see #setCoordinates(Double,Double) */ public double longitude() { return longitude; }; private double longitude; /** Sets the latitude and longitude. * * @see #latitude() * @see #longitude() */ public void setCoordinates( Double newLatitude, Double newLongitude ) { latitude = newLatitude; longitude = newLongitude; } /** The time at which this geocode was last stored to the table, in milliseconds since * the 'epoch'; or zero, if it was never stored. This is intended to implement a * rolling turn-over of the cache, keeping it refeshed with relatively recent values * (say no older than five years) from the geocoding service - but that has not been * implemented yet. * * @see System#currentTimeMillis() */ public final long timestamp() { return timestamp; } private long timestamp = 0L; // - O b j e c t ---------------------------------------------------------------------- /** Returns a descripion of this geocode, including the address and its geographic * coordinates. */ public @Override String toString() { return "Geocode[" + address + " (" + latitude + ", " + longitude + ")]"; } // ==================================================================================== /** Thrown when a geocoding-specific IO exception occurs. */ public static final class GeocodingException extends IOException { public GeocodingException( String message ) { super( message ); } } // ==================================================================================== /** An account with the geocoding service of the Google Maps API. * * @see http://code.google.com/apis/maps/ */ public static @ThreadSafe final class GoogleGeocoding { /** Constructs a GoogleGeocoding. * * @param key per {@linkplain #key key}() */ public GoogleGeocoding( String key ) { this.key = key; } /** The account key, for access to the geocoding service. */ public String key() { return key; } private final String key; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// // ==================================================================================== /** The geocode table of an electoral subserver, caching geocode data in relational * form. */ public static @ThreadSafe final class Table { /** Constructs a Table, physical creating it if it does not already exist. * * @param d database per {@linkplain #database() database}() */ Table( Database d ) throws SQLException { database = d; 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 + "\"" + " (address character varying PRIMARY KEY," + " latitude double precision NOT NULL," + " longitude double precision NOT NULL," + " timestamp bigint NOT NULL)" ); database.statementCache().put( key, s ); } s.execute(); } } } // -------------------------------------------------------------------------------- /** 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; //// 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; } /** Retrieves a geocode from the table, initializing the fields of a Geocode * instance. * * @param geocode the instance to initialize - all fields are * assumed to be at default values * * @return true if geocode retrieved; false if the table has none */ private boolean get( final String address, Geocode geocode ) throws SQLException { final String key = statementKeyBase + "get"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT latitude,longitude,timestamp" + " FROM \"" + tableName + "\"" + " WHERE address = ?" ); database.statementCache().put( key, s ); } s.setString( 1, address ); final ResultSet r = s.executeQuery(); try { if( !r.next() ) return false; geocode.latitude = r.getDouble( 1 ); geocode.longitude = r.getDouble( 2 ); geocode.timestamp = r.getLong( 3 ); return true; } finally{ r.close(); } } } /** Stores a geocode to this table. */ private void put( final Geocode geocode ) throws SQLException { synchronized( database ) { { final String key = statementKeyBase + "putU"; PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "UPDATE \"" + tableName + "\"" + " SET latitude = ?, longitude = ?, timestamp = ?" + " WHERE address = ?" ); database.statementCache().put( key, s ); } s.setDouble( 1, geocode.latitude ); s.setDouble( 2, geocode.longitude ); s.setLong( 3, geocode.timestamp ); s.setString( 4, geocode.address ); 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 + "\"" + " (address, latitude, longitude, timestamp) VALUES (?, ?, ?, ?)" ); database.statementCache().put( key, s ); } s.setString( 1, geocode.address ); s.setDouble( 2, geocode.latitude ); s.setDouble( 3, geocode.longitude ); s.setLong( 4, geocode.timestamp ); s.executeUpdate(); } } } // /** Removes a geocode from the table, if it is stored there. // */ // private void remove( final String address ) 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 address = ?" ); // database.statementCache().put( key, s ); // } // s.setString( 1, address ); // s.executeUpdate(); // } // } private static final String tableName = "geocode"; private static final String statementKeyBase = Table.class.getName() + ":" + tableName + "."; } }