regentanz 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +5 -0
  2. data/CHANGELOG.rdoc +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +24 -0
  5. data/README.rdoc +46 -0
  6. data/Rakefile +23 -0
  7. data/lib/regentanz/astronomy.rb +69 -0
  8. data/lib/regentanz/cache/base.rb +51 -0
  9. data/lib/regentanz/cache/file.rb +86 -0
  10. data/lib/regentanz/cache.rb +2 -0
  11. data/lib/regentanz/callbacks.rb +18 -0
  12. data/lib/regentanz/conditions/base.rb +16 -0
  13. data/lib/regentanz/conditions/current.rb +14 -0
  14. data/lib/regentanz/conditions/forecast.rb +14 -0
  15. data/lib/regentanz/conditions.rb +3 -0
  16. data/lib/regentanz/configuration.rb +55 -0
  17. data/lib/regentanz/configurator.rb +22 -0
  18. data/lib/regentanz/google_weather.rb +151 -0
  19. data/lib/regentanz/parser/google_weather.rb +100 -0
  20. data/lib/regentanz/parser.rb +1 -0
  21. data/lib/regentanz/test_helper.rb +52 -0
  22. data/lib/regentanz/version.rb +4 -0
  23. data/lib/regentanz.rb +12 -0
  24. data/regentanz.gemspec +31 -0
  25. data/test/factories.rb +8 -0
  26. data/test/support/support_mailer.rb +7 -0
  27. data/test/support/tmp/.gitignore +1 -0
  28. data/test/support/valid_response.xml.erb +26 -0
  29. data/test/test_helper.rb +18 -0
  30. data/test/unit/astronomy_test.rb +26 -0
  31. data/test/unit/cache/base_test.rb +53 -0
  32. data/test/unit/cache/file_test.rb +141 -0
  33. data/test/unit/callbacks_test.rb +27 -0
  34. data/test/unit/configuration_test.rb +57 -0
  35. data/test/unit/current_condition_test.rb +33 -0
  36. data/test/unit/forecast_condition_test.rb +35 -0
  37. data/test/unit/google_weather_test.rb +131 -0
  38. data/test/unit/parser/google_weather_parser_test.rb +71 -0
  39. metadata +219 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ pkg
3
+ *~
4
+ *.swp
5
+ Gemfile.lock
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,22 @@
1
+ == CHANGELOG
2
+
3
+ === v0.1.4 // 2011-10-14
4
+ * Fixed bug that caused forecast to have the current day only
5
+ * present? now properly returns its true state (as opposed to always false)
6
+ * Known issue: {current_date_time node always returns the beginning of The Epoch}[https://github.com/carpodaster/regentanz/issues/1] (API problem)
7
+
8
+ === v0.1.2 // 2011-04-19
9
+ * Retry state is tracked by cache backend
10
+ * Removed OpenStruct by adding Conditions::Forecast and Conditions::Current
11
+ * XML-Parsing is down in parser class
12
+
13
+ === v0.1.0 // 2011-04-16
14
+ * Moved caching into separate module
15
+
16
+ === v0.0.5 // 2011-04-15
17
+ * Removed RAILS_ROOT
18
+ * Moved constants into class-wide configuration
19
+ * Callback-stubs
20
+
21
+ === v0.0.1 // 2011-04-13
22
+ * Initially packaged model as a gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in regentanz.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2011, Carsten Zimmermann
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the original author / copyright holder nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.rdoc ADDED
@@ -0,0 +1,46 @@
1
+ == Regentanz
2
+ *Regentanz* (German: <i>rain dance</i>) is a Ruby library to connect to Google's
3
+ innofficial (ie. undocumented and unsupported) weather API.
4
+
5
+ === Installation
6
+ The gem is not published yet and can be installed from github:
7
+ git clone git://github.com/kaupertmedia/regentanz.git
8
+ cd regentanz
9
+ gem build regentanz.gemspec
10
+ gem install regentanz-<gemversion>.gem
11
+
12
+ Much easier using bundler:
13
+ # Gemfile
14
+ gem 'regentanz', :git => "git://github.com/carpodaster/regentanz"
15
+
16
+ === Usage
17
+ Supply a location and a language to retrieve weather:
18
+ weather = Regentanz::GoogleWeather.new(:location => "Berlin, Germany", :lang => :en)
19
+ weather.current # current condition
20
+ weather.forecast # array with forecast conditions
21
+
22
+ It uses <b>file-based caching</b> by default, other cache backends will follow.
23
+ See {Regentanz::Cache::Base}[https://github.com/carpodaster/regentanz/blob/master/lib/regentanz/cache/base.rb]
24
+ for details (and if you want to create your own backend).
25
+
26
+ === Configuration
27
+ *Regentanz* can either be configured through a configure block or directly via
28
+ its configuration object. It uses sane defaults so there should be no need for
29
+ configuration to start right off. If you're using *Regentanz* with Rails,
30
+ a file in <tt>config/initializers</tt> is your friend.
31
+
32
+ Configure block:
33
+ Regentanz.configure do |config|
34
+ config.cache_backend Regentanz::Cache::File
35
+ config.cache_dir "/path/to/cache_file"
36
+ end
37
+
38
+ Direct configuration:
39
+ Regentanz.configuration.cache_dir = "/some/other/path"
40
+
41
+ See {Regentanz::Configuration}[https://github.com/carpodaster/regentanz/blob/master/lib/regentanz/configuration.rb]
42
+ for a full list of configurable options.
43
+
44
+ === Credits
45
+ *Regentanz* is based upon and extracted from a standalone Ruby class made for
46
+ {berlin.kauperts.de}[http://berlin.kauperts.de] by {kaupert media gmbh}[http://kaupertmedia.de].
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+ require 'rake/testtask'
5
+ require 'rake/rdoctask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Regentanz'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README*')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,69 @@
1
+ module Regentanz
2
+ # :nodoc:
3
+ # Code taken from http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/264573
4
+ module Astronomy
5
+ include Math
6
+
7
+ private
8
+ # :doc:
9
+
10
+ def sun_rise_set(mode, lat, lng, zenith = 90.8333)
11
+ #step 1: first calculate the day of the year
12
+ mode = mode.to_sym
13
+ date = Date.today
14
+ n=date.yday
15
+
16
+ #step 2: convert the longitude to hour value and calculate an approximate time
17
+ lng_hour=lng/15
18
+ t = n+ ((6-lng_hour)/24) if mode==:sunrise
19
+ t = n+ ((18-lng_hour)/24) if mode==:sunset
20
+
21
+ #step 3: calculate the sun's mean anomaly
22
+ m = (0.9856 * t) - 3.289
23
+
24
+ #step 4: calculate the sun's true longitude
25
+ l = (m+(1.1916 * sin(deg_to_rad(m))) + (0.020 * sin(deg_to_rad(2*m))) + 282.634) % 360
26
+
27
+ #step 5a: calculate the sun's right ascension
28
+ ra = rad_to_deg(atan(0.91764 * tan(deg_to_rad(l)))) % 360
29
+
30
+ #step 5b: right ascension value needs to be in the same quadrant as L
31
+ lquadrant = (l/90).floor*90
32
+ raquadrant = (ra/90).floor*90
33
+ ra = ra+(lquadrant-raquadrant)
34
+
35
+ #step 5c: right ascension value needs to be converted into hours
36
+ ra/=15
37
+
38
+ #step 6: calculate the sun's declination
39
+ sin_dec = 0.39782 * sin(deg_to_rad(l))
40
+ cos_dec = cos(asin(sin_dec))
41
+ #step 7a: calculate the sun's local hour angle
42
+ cos_h = (cos(deg_to_rad(zenith)) - (sin_dec * sin(deg_to_rad(lat)))) / (cos_dec * cos(deg_to_rad(lat)))
43
+
44
+ return nil if (not (-1..1).include? cos_h)
45
+
46
+ #step 7b: finish calculating H and convert into hours
47
+ h = (360 - rad_to_deg(acos(cos_h)))/15 if mode==:sunrise
48
+ h = (rad_to_deg(acos(cos_h)))/15 if mode==:sunset
49
+
50
+ #step 8: calculate local mean time
51
+ t = h + ra - (0.06571 * t) - 6.622
52
+ t %=24
53
+
54
+ #step 9: convert to UTC
55
+ return (date.to_datetime+(t - lng_hour)/24).to_time.getlocal
56
+ end
57
+
58
+ # Convenience helper
59
+ def deg_to_rad(degrees)
60
+ degrees*PI/180
61
+ end
62
+
63
+ # Convenience helper
64
+ def rad_to_deg(radians)
65
+ radians*180/PI
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ module Regentanz
2
+
3
+ module Cache
4
+
5
+ class Base
6
+
7
+ # Checks if +instance_or_class+ complies to cache backend duck type,
8
+ # ie. if it reponds to all mandatory methods
9
+ def self.lint(instance_or_class)
10
+ instance = instance_or_class.is_a?(Class) ? instance_or_class.new : instance_or_class
11
+ [:set, :get, :available?, :expire!, :valid?].inject(true) do |memo, method|
12
+ memo && instance.respond_to?(method)
13
+ end
14
+ end
15
+
16
+ # Returns a alpha-numeric cache key
17
+ def self.sanitize_key(key)
18
+ Digest::SHA1.hexdigest(key.to_s)
19
+ end
20
+
21
+ # Stores cache +value+ as +key+.
22
+ def set(key, value); end
23
+
24
+ # Retrieves cached value from +key+.
25
+ def get(key); end
26
+
27
+ # Checks if cache under +key+ is available.
28
+ def available?(key); end
29
+
30
+ # Deletes cache under +key+.
31
+ def expire!(key); end
32
+
33
+ # Checks if cache under +key+ is still valid.
34
+ def valid?(key); end
35
+
36
+ # Returns whether or not weather retrieval from the API
37
+ # is currently waiting for a timeout to expire
38
+ def waiting_for_retry?; end
39
+
40
+ # Checks if we've waited enough. Unsets a possible retry
41
+ # state (and returns true) if so or returns false if not
42
+ def unset_retry_state!; end
43
+
44
+ # Persists a timeout state
45
+ def set_retry_state!; end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,86 @@
1
+ module Regentanz
2
+
3
+ module Cache
4
+
5
+ # Implements file-based caching. Cache files are stored in
6
+ # +Regentanz.configuration.cache_dir+ and are prefixed with
7
+ # +Regentanz.configuration.cache_prefix+.
8
+ class File < Regentanz::Cache::Base
9
+
10
+ # Cache is available (n.b. not necessarily #valid?) if a file
11
+ # exists for +key+
12
+ def available?(key)
13
+ ::File.exists?(filename(key)) rescue nil
14
+ end
15
+
16
+ # Unlinks the cache file for +key+
17
+ def expire!(key)
18
+ return false unless available?(key)
19
+ begin
20
+ ::File.delete(filename(key))
21
+ true
22
+ rescue
23
+ end
24
+ end
25
+
26
+ def filename(key)
27
+ ::File.join(Regentanz.configuration.cache_dir, "#{Regentanz.configuration.cache_prefix}_#{key}.xml")
28
+ end
29
+
30
+ # Retrieves content of #filename for +key+
31
+ def get(key)
32
+ if available?(key)
33
+ ::File.open(filename(key), "r") { |file| file.read } rescue nil
34
+ end
35
+ end
36
+
37
+ # Stores +value+ in #filename for +key+
38
+ def set(key, value)
39
+ begin
40
+ ::File.open(filename(key), "w") { |file| file.puts value }
41
+ filename(key)
42
+ rescue
43
+ end
44
+ end
45
+
46
+ def valid?(key)
47
+ return false unless available?(key)
48
+ begin
49
+ # TODO delegate XML parsing and verification
50
+ doc = REXML::Document.new(get(key))
51
+ node = doc.elements["xml_api_reply/weather/forecast_information/current_date_time"]
52
+ time = node.attribute("data").to_s.to_time if node
53
+ time > Regentanz.configuration.cache_ttl.seconds.ago
54
+ rescue
55
+ # TODO pass exception upstream until properly delegated in the first place?
56
+ end
57
+ end
58
+
59
+ # Returns whether or not weather retrieval from the API
60
+ # is currently waiting for a timeout to expire; here: existence of
61
+ # a retry marker file
62
+ def waiting_for_retry?
63
+ ::File.exists?(Regentanz.configuration.retry_marker)
64
+ end
65
+
66
+ # Checks if we've waited long enough. Deletes a possible retry
67
+ # marker file (and returns true) if so or returns false if not
68
+ def unset_retry_state!
69
+ marker = Regentanz.configuration.retry_marker
70
+ if waiting_for_retry? and ::File.new(marker).mtime < Regentanz.configuration.retry_ttl.seconds.ago
71
+ ::File.delete(marker) if ::File.exists?(marker)
72
+ true
73
+ end
74
+ end
75
+
76
+ # Persists the timeout state by writing a retry_marker file
77
+ def set_retry_state!
78
+ ::File.open(Regentanz.configuration.retry_marker, "w+").close
79
+ waiting_for_retry?
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,2 @@
1
+ require 'regentanz/cache/base'
2
+ require 'regentanz/cache/file'
@@ -0,0 +1,18 @@
1
+ module Regentanz
2
+ module Callbacks
3
+
4
+ CALLBACKS = [:api_failure_detected, :api_failure_resumed]
5
+
6
+ CALLBACKS.each do |callback_method|
7
+ # Define no-op stubs for all CALLBACKS
8
+ define_method(callback_method) {}
9
+ private callback_method
10
+ end
11
+
12
+ def self.included(base)
13
+ base.send :include, ActiveSupport::Callbacks
14
+ base.define_callbacks *CALLBACKS
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Regentanz
2
+ module Conditions
3
+ class Base
4
+
5
+ attr_accessor :condition, :style, :icon
6
+
7
+ def initialize(attributes = {})
8
+ attributes.symbolize_keys!
9
+ attributes.keys.each do |attr|
10
+ self.send(:"#{attr}=", attributes[attr]) if respond_to?(:"#{attr}=")
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Regentanz
2
+ module Conditions
3
+
4
+ class Current < Regentanz::Conditions::Base
5
+
6
+ attr_reader :temp_c, :temp_f
7
+ attr_accessor :humidity, :wind_condition
8
+
9
+ def temp_c=temp; @temp_c = temp.to_i; end
10
+ def temp_f=temp; @temp_f = temp.to_i; end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Regentanz
2
+ module Conditions
3
+
4
+ class Forecast < Regentanz::Conditions::Base
5
+
6
+ attr_reader :high, :low
7
+ attr_accessor :day_of_week
8
+
9
+ def high=temp; @high = temp.to_i; end
10
+ def low=temp; @low = temp.to_i; end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ require 'regentanz/conditions/base'
2
+ require 'regentanz/conditions/current'
3
+ require 'regentanz/conditions/forecast'
@@ -0,0 +1,55 @@
1
+ module Regentanz
2
+ require "tmpdir"
3
+
4
+ class Configuration
5
+
6
+ DEFAULT_OPTIONS = [
7
+ :base_url,
8
+ :cache_backend,
9
+ :cache_dir,
10
+ :cache_prefix,
11
+ :cache_ttl,
12
+ :retry_marker,
13
+ :retry_ttl
14
+ ]
15
+
16
+ OPTIONS = DEFAULT_OPTIONS + [
17
+ :do_not_get_weather,
18
+ :suppress_stderr_output
19
+ ]
20
+
21
+ # Define default values
22
+ @@default_base_url = "http://www.google.com/ig/api"
23
+ @@default_cache_backend = Regentanz::Cache::File
24
+ @@default_cache_dir = Dir.tmpdir
25
+ @@default_cache_prefix = "regentanz"
26
+ @@default_cache_ttl = 14400 # 4 hours
27
+ @@default_retry_ttl = 3600 # 1 hour
28
+ @@default_retry_marker = File.join(@@default_cache_dir, "#{@@default_cache_prefix}_api_retry.txt")
29
+
30
+ OPTIONS.each { |opt| attr_accessor(opt) }
31
+ DEFAULT_OPTIONS.each { |cvar| cattr_reader(:"default_#{cvar}", :instance_reader => false) } # class getter for all DEFAULT_OPTION cvars
32
+
33
+ # Stores global configuration information for +Regentanz+.
34
+ #
35
+ # == Default Options
36
+ # * +base_url+: HTTP API, request-specific calls will be appended (default: +http://www.google.com/ig/api+)
37
+ # * +cache_backend+: defaults to +Regentanz::Cache::File+
38
+ # * +cache_dir+: defaults to +Dir.tmpdir+
39
+ # * +cache_prefix+: String to prefix both cache file and retry_marker (if not specified otherwise) with
40
+ # * +cache_ttl+: time in seconds for which cache data is considered valid. Default: 14400 (4 hours).
41
+ # * +retry_ttl+: time in seconds Regentanz should wait until it tries to call the API again when it failed before. Default: 3600 (1 hour).
42
+ # * +retry_marker+: persist a marker-file here to indicate a failed API state. Supply a full pathname.
43
+ #
44
+ # == Options
45
+ # * +do_not_get_weather+: don't try to retrieve weather data, neither from cache nor remote API. Intended for testing.
46
+ # * +suppress_stderr_output+: called from GoogleWeather#error_output, silences Regentanz' output. Intended for testing.
47
+ def initialize(*args)
48
+ DEFAULT_OPTIONS.each do |option|
49
+ self.send(:"#{option}=", self.class.send(:class_variable_get, :"@@default_#{option}") )
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,22 @@
1
+ module Regentanz
2
+ class << self
3
+
4
+ attr_writer :configuration
5
+ def configuration #:nodoc:
6
+ @configuration ||= Configuration.new
7
+ end
8
+
9
+ # Call this method to modify defaults in your initializers.
10
+ # See Regentanz::Configuration for supported config options.
11
+ #
12
+ # === Example usage
13
+ # Regentanz.configure do |config|
14
+ # config.<supported_config_option> = :bar
15
+ # end
16
+ def configure
17
+ self.configuration ||= Configuration.new
18
+ yield(configuration)
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,151 @@
1
+ module Regentanz
2
+ class GoogleWeather
3
+ require 'net/http'
4
+ require 'rexml/document'
5
+ require 'ostruct'
6
+
7
+ include Astronomy
8
+ include Callbacks
9
+
10
+ attr_accessor :location, :cache_id
11
+ attr_reader :cache, :current, :forecast, :lang, :parser, :xml
12
+
13
+ # Creates an object and queries the weather API.
14
+ #
15
+ # === Parameters
16
+ # * +options+ A hash
17
+ #
18
+ # === Available options
19
+ # * +location+ String to pass to Google's weather API (mandatory)
20
+ # * +cache_id+ enables caching with a unique identifier to locate a cached file
21
+ # * +geodata+ a hash with keys :lat and :lng, required for sunset/-rise calculations
22
+ # * +lang+ Desired language for the returned results (Defaults to "de")
23
+ def initialize(*args)
24
+ options = args.extract_options!
25
+ @options = options.symbolize_keys
26
+ @parser = Parser::GoogleWeather.new
27
+ self.location = args.first || options[:location]
28
+ self.lang = options[:lang]
29
+
30
+ # Activate caching
31
+ if Regentanz.configuration.cache_backend
32
+ @cache = Regentanz.configuration.cache_backend.new if Regentanz.configuration.cache_backend
33
+ @cache_id = options[:cache_id] || Regentanz.configuration.cache_backend.sanitize_key(@location)
34
+ end
35
+
36
+ @geodata = options[:geodata] if options[:geodata] and options[:geodata][:lat] and options[:geodata][:lng]
37
+ get_weather() unless Regentanz.configuration.do_not_get_weather
38
+ end
39
+
40
+ # Loads weather data from known data sources (ie. cache or external API)
41
+ def get_weather!; get_weather(); end
42
+
43
+ # Input sanitizer-setter, defaults to "de"
44
+ def lang=(lang)
45
+ @lang = lang.present? ? lang.to_s : "de"
46
+ end
47
+
48
+ # Provide an accessor to see if we actually got weather info at all.
49
+ def present?
50
+ current.present? or forecast.present?
51
+ end
52
+
53
+ def sunrise
54
+ @sunrise ||= @geodata.blank? ? nil : sun_rise_set(:sunrise, @geodata[:lat], @geodata[:lng])
55
+ end
56
+
57
+ def sunset
58
+ @sunset ||= @geodata.blank? ? nil : sun_rise_set(:sunset, @geodata[:lat], @geodata[:lng])
59
+ end
60
+
61
+ def waiting_for_retry?
62
+ @cache && @cache.waiting_for_retry?
63
+ end
64
+
65
+ private
66
+
67
+ # Encapsulate output of error messages. Will output to $stderr unless
68
+ # Regentanz.configuration.suppress_stderr_output is set
69
+ #
70
+ # === Parameters
71
+ # * +output+: String to output
72
+ def error_output(output)
73
+ $stderr.puts output unless Regentanz.configuration.suppress_stderr_output
74
+ end
75
+
76
+ # Proxies +do_request+ and +parse_request+
77
+ def get_weather
78
+ if cache_valid?
79
+ @xml = @parser.convert_encoding(@cache.get(@cache_id))
80
+ else
81
+ @xml = @parser.convert_encoding(do_request(Regentanz.configuration.base_url + "?weather=#{CGI::escape(@location)}&hl=#{@lang}"))
82
+ if @cache
83
+ @cache.expire!(@cache_id)
84
+ @cache.set(@cache_id, @xml)
85
+ end
86
+ end
87
+ parse_xml() if @xml
88
+ end
89
+
90
+ # Makes an outbound HTTP-request and returns the request body (ie. the XML)
91
+ #
92
+ # === Parameters
93
+ # * +url+ API-URL with protocol, fqdn and URI
94
+ def do_request(url)
95
+ begin
96
+ Net::HTTP.get_response(URI.parse(url)).body
97
+ rescue => e
98
+ error_output(e.message)
99
+ end
100
+ end
101
+
102
+ # Parses the raw data and Sets the +current+ and +forecast+ variables.
103
+ #
104
+ # Assumes the instance var +xml+ is set.
105
+ def parse_xml
106
+ begin
107
+ @current = @parser.parse_current!(@xml)
108
+ @forecast = @parser.parse_forecast!(@xml)
109
+ rescue REXML::ParseException => e
110
+ error_output(e.message)
111
+ end
112
+ end
113
+
114
+ # Returns +true+ if a given cache file contains data not older than Regentanz::Configuration#cache_ttl seconds
115
+ # TODO properly use cache backend's #valid?
116
+ def cache_valid?
117
+ validity = false
118
+ if @cache and @cache.available?(@cache_id)
119
+ begin
120
+ doc = REXML::Document.new(@cache.get(@cache_id))
121
+ node = doc.elements["xml_api_reply/weather/forecast_information/current_date_time"]
122
+ time = node.attribute("data").to_s.to_time if node
123
+ validity = time ? time > Regentanz.configuration.cache_ttl.seconds.ago : false
124
+ rescue REXML::ParseException
125
+ retry_after_incorrect_api_reply
126
+ validity = waiting_for_retry? # not really valid, but we need to wait a bit.
127
+ end
128
+ end
129
+ validity
130
+ end
131
+
132
+ # Brings the outbound API-calls to a halt for Regentanz::Configuration#retry_ttl seconds by creating
133
+ # a marker file. Flushes the incorrect cached API response after Regentanz::Configuration#retry_ttl
134
+ # seconds.
135
+ def retry_after_incorrect_api_reply
136
+ if !waiting_for_retry? and @cache
137
+ # We are run for the first time, create the marker file
138
+ # TODO remove dependency to SupportMailer class
139
+ api_failure_detected # callback
140
+ SupportMailer.deliver_weather_retry_marker_notification!(self, :set)
141
+ @cache.set_retry_state!
142
+ elsif @cache and @cache.unset_retry_state!
143
+ # Marker file is old enough, delete the (invalid) cache file and remove the marker_file
144
+ @cache.expire!(@cache_id)
145
+ api_failure_resumed # callback
146
+ SupportMailer.deliver_weather_retry_marker_notification!(self, :unset)
147
+ end
148
+ end
149
+
150
+ end
151
+ end