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);
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 }