View Javadoc

1   package geo.google;
2   
3   import geo.google.datamodel.GeoAddress;
4   import geo.google.datamodel.GeoCoordinate;
5   import geo.google.datamodel.GeoStatusCode;
6   import geo.google.datamodel.GeoUsAddress;
7   import geo.google.mapping.MappingUtils;
8   import geo.google.mapping.XmlMappingFunctor;
9   import geo.google.mapping.XmlToAddressFunctor;
10  import geo.google.mapping.XmlToUsAddressFunctor;
11  
12  import java.text.MessageFormat;
13  import java.util.List;
14  
15  import org.apache.commons.collections.CollectionUtils;
16  import org.apache.commons.httpclient.HttpClient;
17  import org.apache.commons.httpclient.HttpConnectionManager;
18  import org.apache.commons.httpclient.HttpURL;
19  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
20  import org.apache.commons.httpclient.methods.GetMethod;
21  import org.apache.commons.httpclient.params.HttpClientParams;
22  import org.apache.commons.io.IOUtils;
23  
24  /***
25   * Address Standardizer class. 
26   * 
27   * Note: The http connection is synchronized in this class! 
28   * you need to create multiple standardizer if you need concurrency.
29   * <br/>
30   * This class provides a set of methods for standardizing an address.  
31   * <br/>
32   * <p>
33   * Note that this class standardizes the input address by sending a http request to 
34   * google's geocoder service (http://www.google.com/apis/maps/documentation/). 
35   * This service requires an ApiKey which you need to sign up for before you can use this class.
36   * </p>
37   * <p>
38   * There is a geocoding speed limit (from http://googlemapsapi.blogspot.com/2007/09/coming-soon-ip-based-geocode-limiting.html):
39   * <p>
40   * "In the coming week, the Maps API geocode limit will change from a key-based system to an IP-based system, with a new limit of 15,000 queries per day. If you're a developer with a website that's using client-side geocoding via the GClientGeocoder object, this change means that each of your website visitors will now be subject to their own 15K quota. However, if you're a developer using the HTTP geocoder, this change means that all the geocodes from your script will be subject to the same 15K quota (your web server will send the same IP to us with each geocode). We've made this change in our geocoder due to the number of developers who've had issues with the GClientGeocoder and going over quota in times of high mashup user volume." 
41   * </p>
42   * That means if you run at a rate faster than the equivalent of 
43   * 15000 requests per day (5.769 seconds per request) <b>per IP address</b> for several minutes, then Google 
44   * will block you for a day. <b>This class automatically enforces this limit by only sending out
45   * request in 5.769 second interval</b>. You can change the value of this time interval via the constructor. 
46   * </p>
47   * <pre>
48   *  long timeTilNextStart = _rateLimitInterval - ( System.currentTimeMillis() - _lastRequestTime);
49   *  if(timeTilNextStart > 0){
50   *      Thread.sleep(timeTilNextStart); //sleep for some time
51   *  }
52   *  _lastRequestTime = System.currentTimeMillis();
53   * </pre>
54   * For more information about this service, see http://www.google.com/apis/maps/index.html
55   * @author jliang
56   *
57   */
58  public class GeoAddressStandardizer{
59    private static final String BASE_URL = "http://maps.google.com/maps/geo?q={0}&output={1}&key={2}";
60    private static final String XML = "xml", CSV = "csv";
61    private String _apiKey;
62    private long _rateLimitInterval = 5769L;
63    private long _lastRequestTime = System.currentTimeMillis() - _rateLimitInterval;
64    private HttpClientParams _httpClientParams = null;
65    
66    private static HttpConnectionManager _connectionManager = new MultiThreadedHttpConnectionManager();
67    
68    /***
69     * The httpClient in combination with the {@link MultiThreadedHttpConnectionManager} is
70     * thread-safe. See: <a href="http://hc.apache.org/httpclient-3.x/threading.html">HttpClient - Threading</a>
71     */
72    private static HttpClient _httpClient = new HttpClient(_connectionManager);
73  
74  
75    /***
76     * Sets the {@link HttpConnectionManager} to be used for connecting to the geocoding service
77     */
78    public static synchronized void setConnectionManager(HttpConnectionManager manager) {
79  	_connectionManager = manager;
80  	_httpClient = new HttpClient(_connectionManager);
81    }
82     /***
83     * Sets the {@link HttpClient} to be used for connecting to the geocoding service
84     * @param client
85     */
86    public static synchronized void setHttpClient(HttpClient client) {
87      _httpClient = client;
88    }
89  /***
90     * Parameters for controlling the http connection.
91     * http://jakarta.apache.org/commons/httpclient/preference-api.html#HTTP_parameters
92     * @return
93     */
94    public HttpClientParams getHttpClientParams() {
95      return _httpClientParams;
96    }
97    public void setHttpClientParams(HttpClientParams httpClientParams) {
98      _httpClientParams = httpClientParams;
99      if(_httpClientParams != null && _httpClient != null){
100     	_httpClient.setParams(_httpClientParams);
101       }
102   }
103   /***
104    * Register a google geocoding API key at 
105    * http://www.google.com/apis/maps/signup.html
106    */
107   public GeoAddressStandardizer(String apiKey){
108     _apiKey = apiKey;
109   }
110   /***
111    * Register a google geocoding API key at 
112    * http://www.google.com/apis/maps/signup.html
113    */
114   public GeoAddressStandardizer(String apiKey, long rateIntervalInMillis){
115     this(apiKey);
116     if(rateIntervalInMillis < 0){
117       throw new IllegalArgumentException("rateInterval cannot be negative");
118     }
119     _rateLimitInterval = rateIntervalInMillis;
120   }
121   
122   
123   /***
124    * Standardize an address using google's geocoding service;
125    * @throws GeoException Indicates something unexpected occurs.
126    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
127    * @deprecated Use {@link #standardizeToGeoAddresses(String)} instead. This method only returns the first
128    * return geocoded address, which is not always the best standardization. 
129    */
130   public GeoAddress standardizeToGeoAddress(GeoUsAddress usAddress) throws GeoException{
131     return standardizeToGeoAddress(usAddress.toAddressLine());
132   }
133   /***
134    * Standardize an address using google's geocoding service;
135    * @throws GeoException Indicates something unexpected occurs.
136    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
137    * @deprecated Use {@link #standardizeToUsGeoAddresses(String)} instead. This method only returns the first
138    * return geocoded address, which is not always the best standardization. 
139    */
140   public GeoUsAddress standardizeToGeoUsAddress(GeoUsAddress usAddress) throws GeoException{
141     return standardizeToGeoUsAddress(usAddress.toAddressLine());
142   }
143   /***
144    * Standardize an address using google's geocoding service; 
145    * @throws GeoException Indicates something unexpected occurs.
146    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
147    * @deprecated Use {@link #standardizeToGeoAddresses(String)} instead. This method only returns the first
148    * return geocoded address, which is not always the best standardization. 
149    */
150   public GeoAddress standardizeToGeoAddress(String addressLine) throws GeoException{
151     List<GeoAddress> ret = standardize(addressLine, XmlToAddressFunctor.getInstance()); 
152     return CollectionUtils.isEmpty(ret)?null:ret.get(0);
153   }
154   /***
155    * Standardize an address using google's geocoding service; 
156    * <br/>
157    * This method returns the <b>FIRST</b> returned geocoded address. 
158    * @throws GeoException Indicates something unexpected occurs.
159    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
160    * @deprecated Use {@link #standardizeToGeoUsAddresses(String)} instead. This method only returns the first
161    * return geocoded address, which is not always the best standardization.
162    */
163   public GeoUsAddress standardizeToGeoUsAddress(String addressLine) throws GeoException{
164     List<GeoUsAddress> ret = standardize(addressLine, XmlToUsAddressFunctor.getInstance()); 
165     return CollectionUtils.isEmpty(ret)?null:ret.get(0);
166   }
167   /***
168    * Standardize an address using google's geocoding service;
169    * @param addressLine
170    * @return zero or more {@link GeoAddress} objects in a {@link List}. 
171    * @throws GeoException Indicates something unexpected occurs.
172    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
173    */
174   public List<GeoUsAddress> standardizeToGeoUsAddresses(String addressLine) throws GeoException{
175     return standardize(addressLine, XmlToUsAddressFunctor.getInstance());
176   }
177   /***
178    * Standardize an address using google's geocoding service;
179    * @param addressLine
180    * @return zero or more {@link GeoAddress} objects in a {@link List}. 
181    * @throws GeoException Indicates something unexpected occurs.
182    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
183    */
184   public List<GeoAddress> standardizeToGeoAddresses(String addressLine) throws GeoException{
185     return standardize(addressLine, XmlToAddressFunctor.getInstance());
186   }
187   /***
188    * Standardize an address using google's geocoding service;
189    * @param addressLine
190    * @return zero or more {@link GeoAddress} objects in a {@link List}. 
191    * @throws GeoException Indicates something unexpected occurs.
192    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
193    */
194   public List<GeoUsAddress> standardizeToGeoUsAddresses(GeoUsAddress usAddress) throws GeoException{
195     return standardize(usAddress.toAddressLine(), XmlToUsAddressFunctor.getInstance());
196   }
197   /***
198    * Standardize an address using google's geocoding service;
199    * @param addressLine
200    * @return zero or more {@link GeoAddress} objects in a {@link List}. 
201    * @throws GeoException Indicates something unexpected occurs.
202    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request.
203    */
204   public List<GeoAddress> standardizeToGeoAddresses(GeoUsAddress usAddress) throws GeoException{
205     return standardize(usAddress.toAddressLine(), XmlToAddressFunctor.getInstance());
206   }  
207   /***
208    * Standardize an address using google's geocoding service;
209    * @throws GeoException Indicates something unexpected occurs.
210    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request. 
211    */
212   public GeoCoordinate standardizeToGeoCoordinate(String addressLine) throws GeoException{
213     try {
214       HttpURL url = new HttpURL(MessageFormat.format(BASE_URL, addressLine, CSV, _apiKey));
215       String res = getServerResponse(url.toString());
216       return MappingUtils.stringToCoordinate(res);
217     }
218     catch (RuntimeException re){
219       throw re;
220     }catch (GeoException e) {
221       throw e;
222     }catch (Exception e) {
223       throw new GeoException(e.getMessage());
224     }
225   }
226   /***
227    * Standardize an address using google's geocoding service;
228    * @param mappingFunction - a mapping function that converts the kml string returned by google's 
229    * geocoding service to any other object type.
230    * @throws GeoException Indicates something unexpected occurs.
231    * It also includes a {@link GeoStatusCode} to signal problems about the status of the geocoding request. 
232    */
233   public <ReturnType> ReturnType standardize(String addressLine, 
234           XmlMappingFunctor<ReturnType> mappingFunction) throws GeoException{
235     try {
236       HttpURL url = new HttpURL(MessageFormat.format(BASE_URL, addressLine, XML, _apiKey));
237       String res = getServerResponse(url.toString());
238       return mappingFunction.execute(res);
239     }
240     catch (RuntimeException re){
241       throw re;
242     }catch (GeoException e) {
243       throw e;
244     }catch (Exception e) {
245       throw new GeoException(e.getMessage());
246     }
247   }
248   private synchronized String getServerResponse(String url) throws Exception{	  
249     GetMethod get = null; 
250     try {
251        long timeTilNextStart = _rateLimitInterval - ( System.currentTimeMillis() - _lastRequestTime);
252        if(timeTilNextStart > 0){
253           Thread.sleep(timeTilNextStart); //sleep for some time
254        }
255        _lastRequestTime = System.currentTimeMillis();
256        get = new GetMethod(url);
257        get.setFollowRedirects(true);
258        _httpClient.executeMethod(get);
259        return IOUtils.toString(get.getResponseBodyAsStream(), get.getRequestCharSet());
260     } finally {
261        if (get != null) get.releaseConnection();
262     }
263   }
264   
265   public String getApiKey() {
266     return _apiKey;
267   }
268 
269   public void setApiKey(String apiKey) {
270     _apiKey = apiKey;
271   }
272 
273   public long getRateLimitInterval() {
274     return _rateLimitInterval;
275   }
276 
277   public void setRateLimitInterval(long rateLimitInterval) {
278     _rateLimitInterval = rateLimitInterval;
279   }
280   
281   
282 }