File: base.rb

package info (click to toggle)
ruby-geocoder 1.5.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 732 kB
  • sloc: ruby: 6,173; makefile: 3
file content (348 lines) | stat: -rw-r--r-- 10,666 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
require 'net/http'
require 'net/https'
require 'uri'

unless defined?(ActiveSupport::JSON)
  begin
    require 'json'
  rescue LoadError
    raise LoadError, "Please install the 'json' or 'json_pure' gem to parse geocoder results."
  end
end

module Geocoder
  module Lookup

    class Base
      def initialize
        @cache = nil
      end

      ##
      # Human-readable name of the geocoding API.
      #
      def name
        fail
      end

      ##
      # Symbol which is used in configuration to refer to this Lookup.
      #
      def handle
        str = self.class.to_s
        str[str.rindex(':')+1..-1].gsub(/([a-z\d]+)([A-Z])/,'\1_\2').downcase.to_sym
      end

      ##
      # 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.
      #
      def search(query, options = {})
        query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
        results(query).map{ |r|
          result = result_class.new(r)
          result.cache_hit = @cache_hit if cache
          result
        }
      end

      ##
      # Return the URL for a map of the given coordinates.
      #
      # Not necessarily implemented by all subclasses as only some lookups
      # also provide maps.
      #
      def map_link_url(coordinates)
        nil
      end

      ##
      # Array containing string descriptions of keys required by the API.
      # Empty array if keys are optional or not required.
      #
      def required_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.
      #
      def query_url(query)
        base_query_url(query) + url_query_string(query)
      end

      ##
      # The working Cache object.
      #
      def cache
        if @cache.nil? and store = configuration.cache
          @cache = Cache.new(store, configuration.cache_prefix)
        end
        @cache
      end

      ##
      # 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.
      #
      def supported_protocols
        [:http, :https]
      end

      private # -------------------------------------------------------------

      ##
      # String which, when concatenated with url_query_string(query)
      # produces the full query URL. Should include the "?" a the end.
      #
      def base_query_url(query)
        fail
      end

      ##
      # An object with configuration data for this particular lookup.
      #
      def configuration
        Geocoder.config_for_lookup(handle)
      end

      ##
      # Object used to make HTTP requests.
      #
      def http_client
        proxy_name = "#{protocol}_proxy"
        if proxy = configuration.send(proxy_name)
          proxy_url = !!(proxy =~ /^#{protocol}/) ? proxy : protocol + '://' + proxy
          begin
            uri = URI.parse(proxy_url)
          rescue URI::InvalidURIError
            raise ConfigurationError,
              "Error parsing #{protocol.upcase} proxy URL: '#{proxy_url}'"
          end
          Net::HTTP::Proxy(uri.host, uri.port, uri.user, uri.password)
        else
          Net::HTTP
        end
      end

      ##
      # Geocoder::Result object or nil on timeout or other error.
      #
      def results(query)
        fail
      end

      def query_url_params(query)
        query.options[:params] || {}
      end

      def url_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).
      #
      def cache_key(query)
        base_query_url(query) + hash_to_query(cache_key_params(query))
      end

      def cache_key_params(query)
        # omit api_key and token because they may vary among requests
        query_url_params(query).reject do |key,value|
          key.to_s.match(/(key|token)/)
        end
      end

      ##
      # Class of the result objects
      #
      def result_class
        Geocoder::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.
      #
      def raise_error(error, message = nil)
        exceptions = configuration.always_raise
        if exceptions == :all or exceptions.include?( error.is_a?(Class) ? error : error.class )
          raise error, message
        else
          false
        end
      end

      ##
      # Returns a parsed search result (Ruby hash).
      #
      def fetch_data(query)
        parse_raw_data fetch_raw_data(query)
      rescue SocketError => err
        raise_error(err) or Geocoder.log(:warn, "Geocoding API connection cannot be established.")
      rescue Errno::ECONNREFUSED => err
        raise_error(err) or Geocoder.log(:warn, "Geocoding API connection refused.")
      rescue Timeout::Error => err
        raise_error(err) or Geocoder.log(:warn, "Geocoding API not responding fast enough " +
          "(use Geocoder.configure(:timeout => ...) to set limit).")
      end

      def parse_json(data)
        if defined?(ActiveSupport::JSON)
          ActiveSupport::JSON.decode(data)
        else
          JSON.parse(data)
        end
      rescue
        unless raise_error(ResponseParseError.new(data))
          Geocoder.log(:warn, "Geocoding API's response was not valid JSON")
          Geocoder.log(:debug, "Raw response: #{data}")
        end
      end

      ##
      # Parses a raw search result (returns hash or array).
      #
      def parse_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.
      #
      def protocol
        "http" + (use_ssl? ? "s" : "")
      end

      def valid_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.
      #
      def fetch_raw_data(query)
        key = cache_key(query)
        if cache and body = cache[key]
          @cache_hit = true
        else
          check_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 possible
          ct = response['content-type']

          if ct && ct['charset']
            charset = ct.split(';').select do |s|
              s['charset']
            end.first.to_s.split('=')
            if charset.length == 2
              body.force_encoding(charset.last) rescue ArgumentError
            end
          end

          if cache and valid_response?(response)
            cache[key] = body
          end
          @cache_hit = false
        end
        body
      end

      def check_response_for_errors!(response)
        if response.code.to_i == 400
          raise_error(Geocoder::InvalidRequest) ||
            Geocoder.log(:warn, "Geocoding API error: 400 Bad Request")
        elsif response.code.to_i == 401
          raise_error(Geocoder::RequestDenied) ||
            Geocoder.log(:warn, "Geocoding API error: 401 Unauthorized")
        elsif response.code.to_i == 402
          raise_error(Geocoder::OverQueryLimitError) ||
            Geocoder.log(:warn, "Geocoding API error: 402 Payment Required")
        elsif response.code.to_i == 429
          raise_error(Geocoder::OverQueryLimitError) ||
            Geocoder.log(:warn, "Geocoding API error: 429 Too Many Requests")
        elsif response.code.to_i == 503
          raise_error(Geocoder::ServiceUnavailable) ||
            Geocoder.log(:warn, "Geocoding API error: 503 Service Unavailable")
        end
      end

      ##
      # Make an HTTP(S) request to a geocoding API and
      # return the response object.
      #
      def make_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) if use_ssl?
          req = Net::HTTP::Get.new(uri.request_uri, configuration.http_headers)
          if configuration.basic_auth[:user] and configuration.basic_auth[:password]
            req.basic_auth(
              configuration.basic_auth[:user],
              configuration.basic_auth[:password]
            )
          end
          client.request(req)
        end
      rescue Timeout::Error
        raise Geocoder::LookupTimeout
      rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ENETUNREACH, Errno::ECONNRESET
        raise Geocoder::NetworkError
      end

      def use_ssl?
        if supported_protocols == [:https]
          true
        elsif supported_protocols == [:http]
          false
        else
          configuration.use_https
        end
      end

      def configure_ssl!(client); end

      def check_api_key_configuration!(query)
        key_parts = query.lookup.required_api_key_parts
        if key_parts.size > Array(configuration.api_key).size
          parts_string = key_parts.size == 1 ? key_parts.first : key_parts
          raise Geocoder::ConfigurationError,
            "The #{query.lookup.name} API requires a key to be configured: " +
            parts_string.inspect
        end
      end

      ##
      # Simulate ActiveSupport's Object#to_query.
      # Removes any keys with nil value.
      #
      def hash_to_query(hash)
        require 'cgi' unless defined?(CGI) && defined?(CGI.escape)
        hash.collect{ |p|
          p[1].nil? ? nil : p.map{ |i| CGI.escape i.to_s } * '='
        }.compact.sort * '&'
      end
    end
  end
end