require'net/http'require'net/https'require'uri'unlessdefined?(ActiveSupport::JSON)beginrequire'json'rescueLoadErrorraiseLoadError,"Please install the 'json' or 'json_pure' gem to parse geocoder results."endendmoduleGeocodermoduleLookupclassBasedefinitialize@cache=nilend### Human-readable name of the geocoding API.#defnamefailend### Symbol which is used in configuration to refer to this Lookup.#defhandlestr=self.class.to_sstr[str.rindex(':')+1..-1].gsub(/([a-z\d]+)([A-Z])/,'\1_\2').downcase.to_symend### Query the geocoding API and return a Geocoder::Result object.# Returns +nil+ on timeout or error.## Takes a search string (eg: "Mississippi Coast Coliseumf, Biloxi, MS",# "205.128.54.202") for geocoding, or coordinates (latitude, longitude)# for reverse geocoding. Returns an array of <tt>Geocoder::Result</tt>s.#defsearch(query,options={})query=Geocoder::Query.new(query,options)unlessquery.is_a?(Geocoder::Query)results(query).map{|r|result=result_class.new(r)result.cache_hit=@cache_hitifcacheresult}end### Return the URL for a map of the given coordinates.## Not necessarily implemented by all subclasses as only some lookups# also provide maps.#defmap_link_url(coordinates)nilend### Array containing string descriptions of keys required by the API.# Empty array if keys are optional or not required.#defrequired_api_key_parts[]end### URL to use for querying the geocoding engine.## Subclasses should not modify this method. Instead they should define# base_query_url and url_query_string. If absolutely necessary to# subclss this method, they must also subclass #cache_key.#defquery_url(query)base_query_url(query)+url_query_string(query)end### The working Cache object.#defcacheif@cache.nil?andstore=configuration.cachecache_options=configuration.cache_options@cache=Cache.new(store,cache_options)end@cacheend### Array containing the protocols supported by the api.# Should be set to [:http] if only HTTP is supported# or [:https] if only HTTPS is supported.#defsupported_protocols[:http,:https]endprivate# -------------------------------------------------------------### String which, when concatenated with url_query_string(query)# produces the full query URL. Should include the "?" a the end.#defbase_query_url(query)failend### An object with configuration data for this particular lookup.#defconfigurationGeocoder.config_for_lookup(handle)end### Object used to make HTTP requests.#defhttp_clientproxy_name="#{protocol}_proxy"ifproxy=configuration.send(proxy_name)proxy_url=!!(proxy=~/^#{protocol}/)?proxy:protocol+'://'+proxybeginuri=URI.parse(proxy_url)rescueURI::InvalidURIErrorraiseConfigurationError,"Error parsing #{protocol.upcase} proxy URL: '#{proxy_url}'"endNet::HTTP::Proxy(uri.host,uri.port,uri.user,uri.password)elseNet::HTTPendend### Geocoder::Result object or nil on timeout or other error.#defresults(query)failenddefquery_url_params(query)query.options[:params]||{}enddefurl_query_string(query)hash_to_query(query_url_params(query).reject{|key,value|value.nil?})end### Key to use for caching a geocoding result. Usually this will be the# request URL, but in cases where OAuth is used and the nonce,# timestamp, etc varies from one request to another, we need to use# something else (like the URL before OAuth encoding).#defcache_key(query)base_query_url(query)+hash_to_query(cache_key_params(query))enddefcache_key_params(query)# omit api_key and token because they may vary among requestsquery_url_params(query).rejectdo|key,value|key.to_s.match(/(key|token)/)endend### Class of the result objects#defresult_classGeocoder::Result.const_get(self.class.to_s.split(":").last)end### Raise exception if configuration specifies it should be raised.# Return false if exception not raised.#defraise_error(error,message=nil)exceptions=configuration.always_raiseifexceptions==:allorexceptions.include?(error.is_a?(Class)?error:error.class)raiseerror,messageelsefalseendend### Returns a parsed search result (Ruby hash).#deffetch_data(query)parse_raw_datafetch_raw_data(query)rescueSocketError=>errraise_error(err)orGeocoder.log(:warn,"Geocoding API connection cannot be established.")rescueErrno::ECONNREFUSED=>errraise_error(err)orGeocoder.log(:warn,"Geocoding API connection refused.")rescueGeocoder::NetworkError=>errraise_error(err)orGeocoder.log(:warn,"Geocoding API connection is either unreacheable or reset by the peer")rescueTimeout::Error=>errraise_error(err)orGeocoder.log(:warn,"Geocoding API not responding fast enough "+"(use Geocoder.configure(:timeout => ...) to set limit).")enddefparse_json(data)ifdefined?(ActiveSupport::JSON)ActiveSupport::JSON.decode(data)elseJSON.parse(data)endrescueunlessraise_error(ResponseParseError.new(data))Geocoder.log(:warn,"Geocoding API's response was not valid JSON")Geocoder.log(:debug,"Raw response: #{data}")endend### Parses a raw search result (returns hash or array).#defparse_raw_data(raw_data)parse_json(raw_data)end### Protocol to use for communication with geocoding services.# Set in configuration but not available for every service.#defprotocol"http"+(use_ssl??"s":"")enddefvalid_response?(response)(200..399).include?(response.code.to_i)end### Fetch a raw geocoding result (JSON string).# The result might or might not be cached.#deffetch_raw_data(query)key=cache_key(query)ifcacheandbody=cache[key]@cache_hit=trueelsecheck_api_key_configuration!(query)response=make_api_request(query)check_response_for_errors!(response)body=response.body# apply the charset from the Content-Type header, if possiblect=response['content-type']ifct&&ct['charset']charset=ct.split(';').selectdo|s|s['charset']end.first.to_s.split('=')ifcharset.length==2body.force_encoding(charset.last)rescueArgumentErrorendendifcacheandvalid_response?(response)cache[key]=bodyend@cache_hit=falseendbodyenddefcheck_response_for_errors!(response)ifresponse.code.to_i==400raise_error(Geocoder::InvalidRequest)||Geocoder.log(:warn,"Geocoding API error: 400 Bad Request")elsifresponse.code.to_i==401raise_error(Geocoder::RequestDenied)||Geocoder.log(:warn,"Geocoding API error: 401 Unauthorized")elsifresponse.code.to_i==402raise_error(Geocoder::OverQueryLimitError)||Geocoder.log(:warn,"Geocoding API error: 402 Payment Required")elsifresponse.code.to_i==429raise_error(Geocoder::OverQueryLimitError)||Geocoder.log(:warn,"Geocoding API error: 429 Too Many Requests")elsifresponse.code.to_i==503raise_error(Geocoder::ServiceUnavailable)||Geocoder.log(:warn,"Geocoding API error: 503 Service Unavailable")endend### Make an HTTP(S) request to a geocoding API and# return the response object.#defmake_api_request(query)uri=URI.parse(query_url(query))Geocoder.log(:debug,"Geocoder: HTTP request being made for #{uri.to_s}")http_client.start(uri.host,uri.port,use_ssl: use_ssl?,open_timeout: configuration.timeout,read_timeout: configuration.timeout)do|client|configure_ssl!(client)ifuse_ssl?req=Net::HTTP::Get.new(uri.request_uri,configuration.http_headers)ifconfiguration.basic_auth[:user]andconfiguration.basic_auth[:password]req.basic_auth(configuration.basic_auth[:user],configuration.basic_auth[:password])endclient.request(req)endrescueTimeout::ErrorraiseGeocoder::LookupTimeoutrescueErrno::EHOSTUNREACH,Errno::ETIMEDOUT,Errno::ENETUNREACH,Errno::ECONNRESETraiseGeocoder::NetworkErrorenddefuse_ssl?ifsupported_protocols==[:https]trueelsifsupported_protocols==[:http]falseelseconfiguration.use_httpsendenddefconfigure_ssl!(client);enddefcheck_api_key_configuration!(query)key_parts=query.lookup.required_api_key_partsifkey_parts.size>Array(configuration.api_key).sizeparts_string=key_parts.size==1?key_parts.first:key_partsraiseGeocoder::ConfigurationError,"The #{query.lookup.name} API requires a key to be configured: "+parts_string.inspectendend### Simulate ActiveSupport's Object#to_query.# Removes any keys with nil value.#defhash_to_query(hash)require'cgi'unlessdefined?(CGI)&&defined?(CGI.escape)hash.collect{|p|p[1].nil??nil:p.map{|i|CGI.escapei.to_s}*'='}.compact.sort*'&'endendendend