amee 2.5.1 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,5 +1,14 @@
1
1
  = Changelog
2
2
 
3
+ == 2.6.0
4
+ * Add auto-retry for certain HTTP failures.
5
+ * Add options to Connection.new for timeout and retry.
6
+ * Add options to amee.yml for timeout and retry.
7
+ * More useful exception reporting in event of failures.
8
+
9
+ == 2.5.1
10
+ * Fix for bug in empty tag parsing
11
+
3
12
  == 2.5.0
4
13
  * Use ActiveSupport::Cache for caching
5
14
  * Add ability to specify cache and debug options in amee.yml for Rails projects.
@@ -17,10 +26,10 @@
17
26
 
18
27
  == 2.2.0
19
28
 
20
- * Add SSL support. SSL connections are now used by default.
21
- * Log4r support
22
- * Support for reading ItemValueDefinitions
29
+ * Add SSL support. SSL connections are now used by default.
30
+ * Log4r support
31
+ * Support for reading ItemValueDefinitions
23
32
  * Include accessors from other objects
24
- * Internal improvements including
33
+ * Internal improvements including
25
34
  * Improved paging support
26
35
  * Tidier code for collections
data/README CHANGED
@@ -107,6 +107,17 @@ To enable caching, pass ':cache => :memory_store' to AMEE::Connection.new, or ad
107
107
  same caching configuration as your Rails app, you can add 'cache: rails' to
108
108
  amee.yml instead. Caching is disabled by default.
109
109
 
110
+ == RETRY / TIMEOUT SUPPORT
111
+
112
+ The AMEE::Connection object can now automatically retry if a request fails due to
113
+ network problems or connection failures. To enable this feature, pass ':retries => 3'
114
+ to AMEE::Connection.new, or add 'retries: 3' to your amee.yml if using Rails. You can
115
+ change the number of retry attempts, 3 is just used as an example above.
116
+
117
+ The Connection object also allows a timeout to be set for requests. By default this is
118
+ set to 60 seconds, but if you want to provide a different value (30 seconds for
119
+ instance), pass ':timeout => 30' to AMEE::Connection.new, or 'timeout: 30' in amee.yml.
120
+
110
121
  == UPGRADING TO VERSION > 2
111
122
 
112
123
  There are a few changes to the API exposed by this gem for version 2. The main
@@ -12,10 +12,14 @@ module AMEE
12
12
  each_page do
13
13
  parse_page
14
14
  end
15
- rescue => err
16
- #raise AMEE::BadData.new("Couldn't load #{self.class.name}.\n#{response} due to #{err}")
17
- raise AMEE::BadData.new("Couldn't load #{self.class.name}.\n#{response}")
15
+ rescue JSONParseError, XMLParseError
16
+ @connection.expire(collectionpath)
17
+ raise AMEE::BadData.new("Couldn't load #{self.class.name}.\n#{@response}")
18
+ rescue AMEE::BadData
19
+ @connection.expire(collectionpath)
20
+ raise
18
21
  end
22
+
19
23
  def parse_page
20
24
  if json
21
25
  jsoncollector.each do |p|
@@ -26,7 +30,7 @@ module AMEE
26
30
  end
27
31
  else
28
32
  doc.xpath(xmlcollectorpath.split('/')[1...-1].join('/')).first or
29
- raise AMEE::BadData.new("Couldn't load #{self.class.name}. parp\n#{response}")
33
+ raise AMEE::BadData.new("Couldn't load #{self.class.name}. \n#{@response}")
30
34
  doc.xpath(xmlcollectorpath).each do |p|
31
35
  obj=klass.new(parse_xml(p))
32
36
  obj.connection = connection
@@ -39,12 +43,23 @@ module AMEE
39
43
 
40
44
  def fetch
41
45
  @options.merge! @pager.options if @pager
42
- @response= @connection.get(collectionpath, @options).body
43
- if @response.is_json?
44
- @json = true
45
- @doc = JSON.parse(@response)
46
- else
47
- @doc = load_xml_doc(@response)
46
+ retries = [1] * connection.retries
47
+ begin
48
+ @response= @connection.get(collectionpath, @options).body
49
+ if @response.is_json?
50
+ @json = true
51
+ @doc = JSON.parse(@response)
52
+ else
53
+ @doc = load_xml_doc(@response)
54
+ end
55
+ rescue JSON::ParserError, Nokogiri::XML::SyntaxError
56
+ @connection.expire(collectionpath)
57
+ if delay = retries.shift
58
+ sleep delay
59
+ retry
60
+ else
61
+ raise
62
+ end
48
63
  end
49
64
  end
50
65
 
@@ -18,6 +18,7 @@ module AMEE
18
18
  @auth_token = nil
19
19
  @format = options[:format] || (defined?(JSON) ? :json : :xml)
20
20
  @amee_source = options[:amee_source]
21
+ @retries = options[:retries] || 0
21
22
  if !valid?
22
23
  raise "You must supply connection details - server, username and password are all required!"
23
24
  end
@@ -52,7 +53,7 @@ module AMEE
52
53
  @http.verify_depth = 5
53
54
  end
54
55
  end
55
- @http.read_timeout = 60
56
+ self.timeout = options[:timeout] || 60
56
57
  @http.set_debug_output($stdout) if options[:enable_debug]
57
58
  @debug = options[:enable_debug]
58
59
  end
@@ -61,13 +62,14 @@ module AMEE
61
62
  attr_reader :server
62
63
  attr_reader :username
63
64
  attr_reader :password
65
+ attr_reader :retries
64
66
 
65
67
  def timeout
66
68
  @http.read_timeout
67
69
  end
68
70
 
69
71
  def timeout=(t)
70
- @http.read_timeout = t
72
+ @http.open_timeout = @http.read_timeout = t
71
73
  end
72
74
 
73
75
  def version
@@ -217,20 +219,22 @@ module AMEE
217
219
  if response.body.include? "would have resulted in a duplicate resource being created"
218
220
  raise AMEE::DuplicateResource.new("The specified resource already exists. This is most often caused by creating an item that overlaps another in time.\nRequest: #{request.method} #{request.path}\n#{request.body}\Response: #{response.body}")
219
221
  else
220
- raise AMEE::UnknownError.new("An error occurred while talking to AMEE: HTTP response code #{response.code}.\nRequest: #{request.method} #{request.path}\n#{request.body}\Response: #{response.body}")
222
+ raise AMEE::BadRequest.new("Bad request. This is probably due to malformed input data.\nRequest: #{request.method} #{request.path}\n#{request.body}\Response: #{response.body}")
221
223
  end
222
- else
223
- raise AMEE::UnknownError.new("An error occurred while talking to AMEE: HTTP response code #{response.code}.\nRequest: #{request.method} #{request.path}\n#{request.body}\Response: #{response.body}")
224
224
  end
225
+ raise AMEE::UnknownError.new("An error occurred while talking to AMEE: HTTP response code #{response.code}.\nRequest: #{request.method} #{request.path}\n#{request.body}\Response: #{response.body}")
225
226
  end
226
227
 
227
228
  def do_request(request, format = @format)
228
229
  # Open HTTP connection
229
230
  @http.start
230
- # Do request
231
231
  begin
232
+ # Set auth token in cookie (and header just in case someone's stripping cookies)
233
+ request['Cookie'] = "authToken=#{@auth_token}"
234
+ request['authToken'] = @auth_token
235
+ # Do request
232
236
  timethen=Time.now
233
- response = send_request(request, format)
237
+ response = send_request(@http, request, format)
234
238
  Logger.log.debug("Requesting #{request.class} at #{request.path} with #{request.body} in format #{format}, taking #{(Time.now-timethen)*1000} miliseconds")
235
239
  end while !response_ok?(response, request)
236
240
  # Return response
@@ -242,16 +246,28 @@ module AMEE
242
246
  @http.finish if @http.started?
243
247
  end
244
248
 
245
- def send_request(request, format = @format)
246
- # Set auth token in cookie (and header just in case someone's stripping cookies)
247
- request['Cookie'] = "authToken=#{@auth_token}"
248
- request['authToken'] = @auth_token
249
+ def send_request(connection, request, format = @format)
249
250
  # Set accept header
250
251
  request['Accept'] = content_type(format)
251
252
  # Set AMEE source header if set
252
253
  request['X-AMEE-Source'] = @amee_source if @amee_source
253
254
  # Do the business
254
- response = @http.request(request)
255
+ retries = [1] * @retries
256
+ begin
257
+ response = connection.request(request)
258
+ # 500-series errors fail early
259
+ if ['502', '503', '504'].include? response.code
260
+ raise AMEE::ConnectionFailed.new("A connection error occurred while talking to AMEE: HTTP response code #{response.code}.\nRequest: #{request.method} #{request.path}")
261
+ end
262
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
263
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, AMEE::ConnectionFailed => e
264
+ if delay = retries.shift
265
+ sleep delay
266
+ retry
267
+ else
268
+ raise
269
+ end
270
+ end
255
271
  # Done
256
272
  response
257
273
  end
@@ -25,37 +25,39 @@ module AMEE
25
25
  def self.from_json(json)
26
26
  # Parse json
27
27
  doc = JSON.parse(json)
28
- data = {}
29
- data[:uid] = doc['dataCategory']['uid']
30
- data[:created] = DateTime.parse(doc['dataCategory']['created'])
31
- data[:modified] = DateTime.parse(doc['dataCategory']['modified'])
32
- data[:name] = doc['dataCategory']['name']
33
- data[:path] = doc['path']
34
- data[:children] = []
35
- data[:pager] = AMEE::Pager.from_json(doc['children']['pager'])
36
- itemdef=doc['dataCategory']['itemDefinition']
37
- data[:itemdef] = itemdef ? itemdef['uid'] : nil
38
- doc['children']['dataCategories'].each do |child|
39
- category_data = {}
40
- category_data[:name] = child['name']
41
- category_data[:path] = child['path']
42
- category_data[:uid] = child['uid']
43
- data[:children] << category_data
44
- end
45
- data[:items] = []
46
- if doc['children']['dataItems']['rows']
47
- doc['children']['dataItems']['rows'].each do |item|
48
- item_data = {}
49
- item.each_pair do |key, value|
50
- item_data[key.to_sym] = value
28
+ begin
29
+ data = {}
30
+ data[:uid] = doc['dataCategory']['uid']
31
+ data[:created] = DateTime.parse(doc['dataCategory']['created'])
32
+ data[:modified] = DateTime.parse(doc['dataCategory']['modified'])
33
+ data[:name] = doc['dataCategory']['name']
34
+ data[:path] = doc['path']
35
+ data[:children] = []
36
+ data[:pager] = AMEE::Pager.from_json(doc['children']['pager'])
37
+ itemdef=doc['dataCategory']['itemDefinition']
38
+ data[:itemdef] = itemdef ? itemdef['uid'] : nil
39
+ doc['children']['dataCategories'].each do |child|
40
+ category_data = {}
41
+ category_data[:name] = child['name']
42
+ category_data[:path] = child['path']
43
+ category_data[:uid] = child['uid']
44
+ data[:children] << category_data
45
+ end
46
+ data[:items] = []
47
+ if doc['children']['dataItems']['rows']
48
+ doc['children']['dataItems']['rows'].each do |item|
49
+ item_data = {}
50
+ item.each_pair do |key, value|
51
+ item_data[key.to_sym] = value
52
+ end
53
+ data[:items] << item_data
51
54
  end
52
- data[:items] << item_data
53
55
  end
56
+ # Create object
57
+ Category.new(data)
58
+ rescue AMEE::JSONParseError
59
+ raise AMEE::BadData.new("Couldn't load DataCategory from JSON data. Check that your URL is correct.\n#{json}")
54
60
  end
55
- # Create object
56
- Category.new(data)
57
- rescue
58
- raise AMEE::BadData.new("Couldn't load DataCategory from JSON data. Check that your URL is correct.\n#{json}")
59
61
  end
60
62
 
61
63
  def self.xmlpathpreamble
@@ -64,39 +66,41 @@ module AMEE
64
66
  def self.from_xml(xml)
65
67
  # Parse XML
66
68
  @doc = doc= REXML::Document.new(xml)
67
- data = {}
68
- data[:uid] = x '@uid'
69
- data[:created] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataCategoryResource/DataCategory/@created").to_s)
70
- data[:modified] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataCategoryResource/DataCategory/@modified").to_s)
71
- data[:name] = (REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/Name') || REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/name')).text
72
- data[:path] = (REXML::XPath.first(doc, '/Resources/DataCategoryResource/Path') || REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/path')).text || ""
73
- data[:pager] = AMEE::Pager.from_xml(REXML::XPath.first(doc, '//Pager'))
74
- itemdefattrib=REXML::XPath.first(doc, '/Resources/DataCategoryResource//ItemDefinition/@uid')
75
- data[:itemdef] = itemdefattrib ? itemdefattrib.to_s : nil
76
- data[:children] = []
77
- REXML::XPath.each(doc, '/Resources/DataCategoryResource//Children/DataCategories/DataCategory') do |child|
78
- category_data = {}
79
- category_data[:name] = (child.elements['Name'] || child.elements['name']).text
80
- category_data[:path] = (child.elements['Path'] || child.elements['path']).text
81
- category_data[:uid] = child.attributes['uid'].to_s
82
- data[:children] << category_data
83
- end
84
- data[:items] = []
85
- REXML::XPath.each(doc, '/Resources/DataCategoryResource//Children/DataItems/DataItem') do |item|
86
- item_data = {}
87
- item_data[:uid] = item.attributes['uid'].to_s
88
- item.elements.each do |element|
89
- item_data[element.name.to_sym] = element.text
69
+ begin
70
+ data = {}
71
+ data[:uid] = x '@uid'
72
+ data[:created] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataCategoryResource/DataCategory/@created").to_s)
73
+ data[:modified] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataCategoryResource/DataCategory/@modified").to_s)
74
+ data[:name] = (REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/Name') || REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/name')).text
75
+ data[:path] = (REXML::XPath.first(doc, '/Resources/DataCategoryResource/Path') || REXML::XPath.first(doc, '/Resources/DataCategoryResource/DataCategory/path')).text || ""
76
+ data[:pager] = AMEE::Pager.from_xml(REXML::XPath.first(doc, '//Pager'))
77
+ itemdefattrib=REXML::XPath.first(doc, '/Resources/DataCategoryResource//ItemDefinition/@uid')
78
+ data[:itemdef] = itemdefattrib ? itemdefattrib.to_s : nil
79
+ data[:children] = []
80
+ REXML::XPath.each(doc, '/Resources/DataCategoryResource//Children/DataCategories/DataCategory') do |child|
81
+ category_data = {}
82
+ category_data[:name] = (child.elements['Name'] || child.elements['name']).text
83
+ category_data[:path] = (child.elements['Path'] || child.elements['path']).text
84
+ category_data[:uid] = child.attributes['uid'].to_s
85
+ data[:children] << category_data
90
86
  end
91
- if item_data[:path].nil?
92
- item_data[:path] = item_data[:uid]
87
+ data[:items] = []
88
+ REXML::XPath.each(doc, '/Resources/DataCategoryResource//Children/DataItems/DataItem') do |item|
89
+ item_data = {}
90
+ item_data[:uid] = item.attributes['uid'].to_s
91
+ item.elements.each do |element|
92
+ item_data[element.name.to_sym] = element.text
93
+ end
94
+ if item_data[:path].nil?
95
+ item_data[:path] = item_data[:uid]
96
+ end
97
+ data[:items] << item_data
93
98
  end
94
- data[:items] << item_data
99
+ # Create object
100
+ Category.new(data)
101
+ rescue AMEE::XMLParseError
102
+ raise AMEE::BadData.new("Couldn't load DataCategory from XML data. Check that your URL is correct.\n#{xml}")
95
103
  end
96
- # Create object
97
- Category.new(data)
98
- rescue
99
- raise AMEE::BadData.new("Couldn't load DataCategory from XML data. Check that your URL is correct.\n#{xml}")
100
104
  end
101
105
 
102
106
  def self.get(connection, path, orig_options = {})
@@ -112,20 +116,12 @@ module AMEE
112
116
  end
113
117
 
114
118
  # Load data from path
115
- response = connection.get(path, options).body
119
+ cat = get_and_parse(connection, path, options)
116
120
 
117
- # Parse data from response
118
- if response.is_json?
119
- cat = Category.from_json(response)
120
- else
121
- cat = Category.from_xml(response)
122
- end
123
121
  # Store connection in object for future use
124
122
  cat.connection = connection
125
123
  # Done
126
124
  return cat
127
- rescue
128
- raise AMEE::BadData.new("Couldn't load DataCategory. Check that your URL is correct.\n#{response}")
129
125
  end
130
126
 
131
127
  def self.root(connection)
@@ -34,102 +34,107 @@ module AMEE
34
34
  def self.from_json(json)
35
35
  # Read JSON
36
36
  doc = JSON.parse(json)
37
- data = {}
38
- data[:uid] = doc['dataItem']['uid']
39
- data[:created] = DateTime.parse(doc['dataItem']['created'])
40
- data[:modified] = DateTime.parse(doc['dataItem']['modified'])
41
- data[:name] = doc['dataItem']['name']
42
- data[:path] = doc['path']
43
- data[:label] = doc['dataItem']['label']
44
- data[:item_definition] = doc['dataItem']['itemDefinition']['uid']
45
- data[:category_uid] = doc['dataItem']['dataCategory']['uid']
46
- # Read v2 total
47
- data[:total_amount] = doc['amount']['value'] rescue nil
48
- data[:total_amount_unit] = doc['amount']['unit'] rescue nil
49
- # Read v1 total
50
- if data[:total_amount].nil?
51
- data[:total_amount] = doc['amountPerMonth'] rescue nil
52
- data[:total_amount_unit] = "kg/month"
53
- end
54
- # Get values
55
- data[:values] = []
56
- doc['dataItem']['itemValues'].each do |value|
57
- value_data = {}
58
- value_data[:name] = value['name']
59
- value_data[:path] = value['path']
60
- value_data[:value] = value['value']
61
- value_data[:drill] = value['itemValueDefinition']['drillDown'] rescue nil
62
- value_data[:uid] = value['uid']
63
- data[:values] << value_data
64
- end
65
- # Get choices
66
- data[:choices] = []
67
- doc['userValueChoices']['choices'].each do |choice|
68
- choice_data = {}
69
- choice_data[:name] = choice['name']
70
- choice_data[:value] = choice['value']
71
- data[:choices] << choice_data
37
+ begin
38
+ data = {}
39
+ data[:uid] = doc['dataItem']['uid']
40
+ data[:created] = DateTime.parse(doc['dataItem']['created'])
41
+ data[:modified] = DateTime.parse(doc['dataItem']['modified'])
42
+ data[:name] = doc['dataItem']['name']
43
+ data[:path] = doc['path']
44
+ data[:label] = doc['dataItem']['label']
45
+ data[:item_definition] = doc['dataItem']['itemDefinition']['uid']
46
+ data[:category_uid] = doc['dataItem']['dataCategory']['uid']
47
+ # Read v2 total
48
+ data[:total_amount] = doc['amount']['value'] rescue nil
49
+ data[:total_amount_unit] = doc['amount']['unit'] rescue nil
50
+ # Read v1 total
51
+ if data[:total_amount].nil?
52
+ data[:total_amount] = doc['amountPerMonth'] rescue nil
53
+ data[:total_amount_unit] = "kg/month"
54
+ end
55
+ # Get values
56
+ data[:values] = []
57
+ doc['dataItem']['itemValues'].each do |value|
58
+ value_data = {}
59
+ value_data[:name] = value['name']
60
+ value_data[:path] = value['path']
61
+ value_data[:value] = value['value']
62
+ value_data[:drill] = value['itemValueDefinition']['drillDown'] rescue nil
63
+ value_data[:uid] = value['uid']
64
+ data[:values] << value_data
65
+ end
66
+ # Get choices
67
+ data[:choices] = []
68
+ doc['userValueChoices']['choices'].each do |choice|
69
+ choice_data = {}
70
+ choice_data[:name] = choice['name']
71
+ choice_data[:value] = choice['value']
72
+ data[:choices] << choice_data
73
+ end
74
+ data[:start_date] = DateTime.parse(doc['dataItem']['startDate']) rescue nil
75
+ # Create object
76
+ Item.new(data)
77
+ rescue
78
+ raise AMEE::BadData.new("Couldn't load DataItem from JSON. Check that your URL is correct.\n#{json}")
72
79
  end
73
- data[:start_date] = DateTime.parse(doc['dataItem']['startDate']) rescue nil
74
- # Create object
75
- Item.new(data)
76
- rescue
77
- raise AMEE::BadData.new("Couldn't load DataItem from JSON. Check that your URL is correct.\n#{json}")
78
80
  end
79
81
 
80
82
  def self.from_xml(xml)
81
83
  # Parse data from response into hash
82
84
  doc = REXML::Document.new(xml)
83
- data = {}
84
- data[:uid] = REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@uid").to_s
85
- data[:created] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@created").to_s)
86
- data[:modified] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@modified").to_s)
87
- data[:name] = (REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/Name') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/name')).text
88
- data[:path] = (REXML::XPath.first(doc, '/Resources/DataItemResource/Path') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/path')).text
89
- data[:label] = (REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/Label') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/label')).text
90
- data[:item_definition] = REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/ItemDefinition/@uid').to_s
91
- data[:category_uid] = REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/DataCategory/@uid').to_s
92
- # Read v2 total
93
- data[:total_amount] = REXML::XPath.first(doc, '/Resources/DataItemResource/Amount').text.to_f rescue nil
94
- data[:total_amount_unit] = REXML::XPath.first(doc, '/Resources/DataItemResource/Amount/@unit').to_s rescue nil
95
- # Read v1 total
96
- if data[:total_amount].nil?
97
- data[:total_amount] = REXML::XPath.first(doc, '/Resources/DataItemResource/AmountPerMonth').text.to_f rescue nil
98
- data[:total_amount_unit] = "kg/month"
99
- end
100
- # Get values
101
- data[:values] = []
102
- REXML::XPath.each(doc, '/Resources/DataItemResource/DataItem/ItemValues/ItemValue') do |value|
103
- value_data = {}
104
- value_data[:name] = (value.elements['Name'] || value.elements['name']).text
105
- value_data[:path] = (value.elements['Path'] || value.elements['path']).text
106
- value_data[:value] = (value.elements['Value'] || value.elements['value']).text
107
- value_data[:drill] = value.elements['ItemValueDefinition'].elements['DrillDown'].text == "false" ? false : true rescue nil
108
- value_data[:uid] = value.attributes['uid'].to_s
109
- data[:values] << value_data
110
- end
111
- # Get choices
112
- data[:choices] = []
113
- REXML::XPath.each(doc, '/Resources/DataItemResource/Choices/Choices/Choice') do |choice|
114
- choice_data = {}
115
- choice_data[:name] = (choice.elements['Name']).text
116
- choice_data[:value] = (choice.elements['Value']).text || ""
117
- data[:choices] << choice_data
85
+ begin
86
+ data = {}
87
+ data[:uid] = REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@uid").to_s
88
+ data[:created] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@created").to_s)
89
+ data[:modified] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/@modified").to_s)
90
+ data[:name] = (REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/Name') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/name')).text
91
+ data[:path] = (REXML::XPath.first(doc, '/Resources/DataItemResource/Path') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/path')).text
92
+ data[:label] = (REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/Label') || REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/label')).text
93
+ data[:item_definition] = REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/ItemDefinition/@uid').to_s
94
+ data[:category_uid] = REXML::XPath.first(doc, '/Resources/DataItemResource/DataItem/DataCategory/@uid').to_s
95
+ # Read v2 total
96
+ data[:total_amount] = REXML::XPath.first(doc, '/Resources/DataItemResource/Amount').text.to_f rescue nil
97
+ data[:total_amount_unit] = REXML::XPath.first(doc, '/Resources/DataItemResource/Amount/@unit').to_s rescue nil
98
+ # Read v1 total
99
+ if data[:total_amount].nil?
100
+ data[:total_amount] = REXML::XPath.first(doc, '/Resources/DataItemResource/AmountPerMonth').text.to_f rescue nil
101
+ data[:total_amount_unit] = "kg/month"
102
+ end
103
+ # Get values
104
+ data[:values] = []
105
+ REXML::XPath.each(doc, '/Resources/DataItemResource/DataItem/ItemValues/ItemValue') do |value|
106
+ value_data = {}
107
+ value_data[:name] = (value.elements['Name'] || value.elements['name']).text
108
+ value_data[:path] = (value.elements['Path'] || value.elements['path']).text
109
+ value_data[:value] = (value.elements['Value'] || value.elements['value']).text
110
+ value_data[:drill] = value.elements['ItemValueDefinition'].elements['DrillDown'].text == "false" ? false : true rescue nil
111
+ value_data[:uid] = value.attributes['uid'].to_s
112
+ data[:values] << value_data
113
+ end
114
+ # Get choices
115
+ data[:choices] = []
116
+ REXML::XPath.each(doc, '/Resources/DataItemResource/Choices/Choices/Choice') do |choice|
117
+ choice_data = {}
118
+ choice_data[:name] = (choice.elements['Name']).text
119
+ choice_data[:value] = (choice.elements['Value']).text || ""
120
+ data[:choices] << choice_data
121
+ end
122
+ data[:start_date] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/StartDate").text) rescue nil
123
+ # Create object
124
+ Item.new(data)
125
+ rescue
126
+ raise AMEE::BadData.new("Couldn't load DataItem from XML. Check that your URL is correct.\n#{xml}")
118
127
  end
119
- data[:start_date] = DateTime.parse(REXML::XPath.first(doc, "/Resources/DataItemResource/DataItem/StartDate").text) rescue nil
120
- # Create object
121
- Item.new(data)
122
- rescue
123
- raise AMEE::BadData.new("Couldn't load DataItem from XML. Check that your URL is correct.\n#{xml}")
124
128
  end
125
129
 
126
130
 
127
131
  def self.get(connection, path, options = {})
128
132
  # Load data from path
129
- response = connection.get(path, options).body
130
- AMEE::Data::Item.parse(connection, response)
131
- rescue
132
- raise AMEE::BadData.new("Couldn't load DataItem. Check that your URL is correct.\n#{response}")
133
+ item= get_and_parse(connection, path, options)
134
+ # Store connection in object for future use
135
+ item.connection = connection
136
+ # Done
137
+ return item
133
138
  end
134
139
 
135
140
  def self.parse(connection, response)
@@ -18,6 +18,9 @@ module AMEE
18
18
  end
19
19
  end
20
20
 
21
+ class BadRequest < Exception
22
+ end
23
+
21
24
  class AuthFailed < Exception
22
25
  end
23
26
 
@@ -42,4 +45,28 @@ module AMEE
42
45
  class Deprecated < Exception
43
46
  end
44
47
 
45
- end
48
+ # These are used to classify exceptions that can occur during parsing
49
+
50
+ module XMLParseError
51
+ end
52
+
53
+ module JSONParseError
54
+ end
55
+
56
+ end
57
+
58
+ # Set up possible XML parse errors
59
+ [
60
+ ArgumentError, # thrown by Date.parse
61
+ ].each do |m|
62
+ m.send(:include, AMEE::XMLParseError)
63
+ end
64
+
65
+ # Set up possible JSON parse errors
66
+ [
67
+ ArgumentError, # thrown by Date.parse,
68
+ NoMethodError, # missing elements in JSON, thrown by nil[]
69
+ ].each do |m|
70
+ m.send(:include, AMEE::JSONParseError)
71
+ end
72
+
@@ -21,6 +21,37 @@ module AMEE
21
21
  def expire_cache
22
22
  @connection.expire_matching(full_path+'.*')
23
23
  end
24
-
24
+
25
+ # A nice shared get/parse handler that handles retry on parse errors
26
+ def self.get_and_parse(connection, path, options)
27
+ # Note that we don't check the number of times retry has been done lower down
28
+ # and count separately instead.
29
+ # Worst case could be retries squared given the right pattern of failure, but
30
+ # that seems unlikely. Would need, for instance, repeated 503 503 200->parse_fail
31
+ retries = [1] * connection.retries
32
+ begin
33
+ # Load data from path
34
+ response = connection.get(path, options).body
35
+ # Parse data from response
36
+ if response.is_json?
37
+ from_json(response)
38
+ else
39
+ from_xml(response)
40
+ end
41
+ rescue JSON::ParserError, Nokogiri::XML::SyntaxError, REXML::ParseException => e
42
+ # Invalid JSON or XML received, try the GET again in case it got cut off mid-stream
43
+ connection.expire(path)
44
+ if delay = retries.shift
45
+ sleep delay
46
+ retry
47
+ else
48
+ raise
49
+ end
50
+ rescue AMEE::BadData
51
+ connection.expire(path)
52
+ raise
53
+ end
54
+ end
55
+
25
56
  end
26
57
  end
@@ -34,10 +34,10 @@ module ParseHelper
34
34
  ''
35
35
  end
36
36
  def load_xml_doc(xml)
37
- doc = Nokogiri.XML(xml)
37
+ doc = Nokogiri.XML(xml) { |config| config.strict }
38
38
  doc.remove_namespaces!
39
39
  doc
40
40
  end
41
41
  private :x
42
-
42
+
43
43
  end
@@ -12,6 +12,12 @@ module AMEE
12
12
  if $AMEE_CONFIG['ssl'] == false
13
13
  options.merge! :ssl => false
14
14
  end
15
+ if $AMEE_CONFIG['retries']
16
+ options.merge! :retries => $AMEE_CONFIG['retries'].to_i
17
+ end
18
+ if $AMEE_CONFIG['timeout']
19
+ options.merge! :timeout => $AMEE_CONFIG['timeout'].to_i
20
+ end
15
21
  if $AMEE_CONFIG['cache'] == 'rails'
16
22
  # Pass in the rails cache store
17
23
  options[:cache_store] = ActionController::Base.cache_store
@@ -2,8 +2,8 @@ module AMEE
2
2
 
3
3
  module VERSION #:nodoc:
4
4
  MAJOR = 2
5
- MINOR = 5
6
- TINY = 1
5
+ MINOR = 6
6
+ TINY = 0
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
9
9
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amee
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
- - 5
9
- - 1
10
- version: 2.5.1
8
+ - 6
9
+ - 0
10
+ version: 2.6.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - James Smith
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-08 00:00:00 +00:00
18
+ date: 2011-02-16 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency