amee 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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