# Copyright (C) 2008-2011 AMEE UK Ltd. - http://www.amee.com # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details. module AMEE module Profile class Item < AMEE::Profile::Object def initialize(data = {}) @values = data ? data[:values] : [] @total_amount = data[:total_amount] @total_amount_unit = data[:total_amount_unit] @amounts = data[:amounts] || [] @notes = data[:notes] || [] @start_date = data[:start_date] || data[:valid_from] @end_date = data[:end_date] || (data[:end] == true ? @start_date : nil ) @data_item_uid = data[:data_item_uid] super end attr_reader :values attr_reader :total_amount attr_reader :total_amount_unit attr_reader :amounts attr_reader :notes attr_reader :start_date attr_reader :end_date attr_reader :data_item_uid # V1 compatibility def valid_from start_date end def end end_date.nil? ? false : start_date == end_date end def duration end_date.nil? ? nil : (end_date - start_date).to_f end def self.from_json(json) # Parse json doc = JSON.parse(json) data = {} data[:profile_uid] = doc['profile']['uid'] data[:data_item_uid] = doc['profileItem']['dataItem']['uid'] data[:uid] = doc['profileItem']['uid'] data[:name] = doc['profileItem']['name'] data[:path] = doc['path'] data[:total_amount] = doc['profileItem']['amountPerMonth'] data[:total_amount_unit] = "kg/month" data[:valid_from] = DateTime.strptime(doc['profileItem']['validFrom'], "%Y%m%d") data[:end] = doc['profileItem']['end'] == "false" ? false : true data[:values] = [] doc['profileItem']['itemValues'].each do |item| value_data = {} item.each_pair do |key,value| case key when 'name', 'path', 'uid', 'value' value_data[key.downcase.to_sym] = value end end data[:values] << value_data end # Create object Item.new(data) rescue raise AMEE::BadData.new("Couldn't load ProfileItem from JSON data. Check that your URL is correct.\n#{json}") end def self.from_v2_json(json) # Parse json doc = JSON.parse(json) data = {} data[:profile_uid] = doc['profile']['uid'] data[:data_item_uid] = doc['profileItem']['dataItem']['uid'] data[:uid] = doc['profileItem']['uid'] data[:name] = doc['profileItem']['name'] data[:path] = doc['path'] data[:total_amount] = doc['profileItem']['amount']['value'].to_f data[:total_amount_unit] = doc['profileItem']['amount']['unit'] data[:start_date] = DateTime.parse(doc['profileItem']['startDate']) data[:end_date] = DateTime.parse(doc['profileItem']['endDate']) rescue nil data[:values] = [] doc['profileItem']['itemValues'].each do |item| value_data = {} item.each_pair do |key,value| case key when 'name', 'path', 'uid', 'value', 'unit' value_data[key.downcase.to_sym] = value when 'perUnit' value_data[:per_unit] = value end end data[:values] << value_data end if doc['profileItem']['amounts'] if doc['profileItem']['amounts']['amount'] data[:amounts] = doc['profileItem']['amounts']['amount'].map do |item| { :type => item['type'], :value => item['value'].to_f, :unit => item['unit'], :per_unit => item['perUnit'], :default => (item['default'] == 'true'), } end end if doc['profileItem']['amounts']['note'] data[:notes] = doc['profileItem']['amounts']['note'].map do |item| { :type => item['type'], :value => item['value'], } end end end # Create object Item.new(data) rescue raise AMEE::BadData.new("Couldn't load ProfileItem from V2 JSON data. Check that your URL is correct.\n#{json}") end def self.from_xml(xml) # Parse XML doc = REXML::Document.new(xml) data = {} data[:profile_uid] = REXML::XPath.first(doc, "/Resources/ProfileItemResource/Profile/@uid").to_s data[:data_item_uid] = REXML::XPath.first(doc, "/Resources/ProfileItemResource/DataItem/@uid").to_s data[:uid] = REXML::XPath.first(doc, "/Resources/ProfileItemResource/ProfileItem/@uid").to_s data[:name] = REXML::XPath.first(doc, '/Resources/ProfileItemResource/ProfileItem/Name').text data[:path] = REXML::XPath.first(doc, '/Resources/ProfileItemResource/Path').text || "" data[:total_amount] = REXML::XPath.first(doc, '/Resources/ProfileItemResource/ProfileItem/AmountPerMonth').text.to_f rescue nil data[:total_amount_unit] = "kg/month" data[:valid_from] = DateTime.strptime(REXML::XPath.first(doc, "/Resources/ProfileItemResource/ProfileItem/ValidFrom").text, "%Y%m%d") data[:end] = REXML::XPath.first(doc, '/Resources/ProfileItemResource/ProfileItem/End').text == "false" ? false : true data[:values] = [] REXML::XPath.each(doc, '/Resources/ProfileItemResource/ProfileItem/ItemValues/ItemValue') do |item| value_data = {} item.elements.each do |element| key = element.name value = element.text case key when 'Name', 'Path', 'Value' value_data[key.downcase.to_sym] = value end end value_data[:uid] = item.attributes['uid'].to_s data[:values] << value_data end # Create object Item.new(data) rescue raise AMEE::BadData.new("Couldn't load ProfileItem from XML data. Check that your URL is correct.\n#{xml}") end def self.xmlpathpreamble "/Resources/ProfileItemResource/" end def self.from_v2_xml(xml) # Parse XML @doc = load_xml_doc(xml) data = {} data[:profile_uid] = x 'Profile/@uid' data[:data_item_uid] = x 'DataItem/@uid' data[:uid] = x 'ProfileItem/@uid' data[:name] = x 'ProfileItem/Name' data[:path] = x('Path') || "" data[:total_amount] = x('ProfileItem/Amount').to_f rescue nil data[:total_amount_unit] = x 'ProfileItem/Amount/@unit' rescue nil data[:start_date] = DateTime.parse(x 'ProfileItem/StartDate') data[:end_date] = DateTime.parse(x 'ProfileItem/EndDate') rescue nil data[:values] = [] @doc.xpath("#{xmlpathpreamble}ProfileItem/ItemValues/ItemValue").each do |item| value_data = {} item.elements.each do |element| key = element.name value = element.text case key when 'Name', 'Path', 'Value', 'Unit' value_data[key.downcase.to_sym] = value.blank? ? nil : value when 'PerUnit' value_data[:per_unit] = value end end value_data[:uid] = item.attributes['uid'].to_s data[:values] << value_data end data[:amounts] = @doc.xpath('/Resources/ProfileItemResource/ProfileItem/Amounts/Amount').map do |item| x = { :type => item.attribute('type').value, :value => item.text.to_f, :unit => item.attribute('unit').value, } x[:per_unit] = item.attribute('perUnit').value if item.attribute('perUnit') x[:default] = (item.attribute('default').value == 'true') if item.attribute('default') x end data[:notes] = @doc.xpath('/Resources/ProfileItemResource/ProfileItem/Amounts/Note').map do |item| { :type => item.attribute('type').value, :value => item.text, } end # Create object Item.new(data) rescue raise AMEE::BadData.new("Couldn't load ProfileItem from V2 XML data. Check that your URL is correct.\n#{xml}") end def self.from_v2_atom(response) # Parse XML doc = REXML::Document.new(response) data = {} data[:profile_uid] = REXML::XPath.first(doc, "/entry/@xml:base").to_s.match("/profiles/(.*?)/")[1] #data[:data_item_uid] = REXML::XPath.first(doc, "/Resources/ProfileItemResource/DataItem/@uid").to_s data[:uid] = REXML::XPath.first(doc, "/entry/id").text.match("urn:item:(.*)")[1] data[:name] = REXML::XPath.first(doc, '/entry/title').text data[:path] = REXML::XPath.first(doc, "/entry/@xml:base").to_s.match("/profiles/.*?(/.*)")[1] data[:total_amount] = REXML::XPath.first(doc, '/entry/amee:amount').text.to_f rescue nil data[:total_amount_unit] = REXML::XPath.first(doc, '/entry/amee:amount/@unit').to_s rescue nil data[:start_date] = DateTime.parse(REXML::XPath.first(doc, "/entry/amee:startDate").text) data[:end_date] = DateTime.parse(REXML::XPath.first(doc, "/entry/amee:endDate").text) rescue nil data[:values] = [] REXML::XPath.each(doc, '/entry/amee:itemValue') do |item| value_data = {} value_data[:name] = item.elements['amee:name'].text value_data[:value] = item.elements['amee:value'].text unless item.elements['amee:value'].text == "N/A" value_data[:path] = item.elements['link'].attributes['href'].to_s value_data[:unit] = item.elements['amee:unit'].text rescue nil value_data[:per_unit] = item.elements['amee:perUnit'].text rescue nil data[:values] << value_data end # Create object Item.new(data) rescue raise AMEE::BadData.new("Couldn't load ProfileItem from V2 ATOM data. Check that your URL is correct.\n#{response}") end def self.parse(connection, response) # Parse data from response if response.is_v2_json? item = Item.from_v2_json(response) elsif response.is_json? item = Item.from_json(response) elsif response.is_v2_atom? item = Item.from_v2_atom(response) elsif response.is_v2_xml? item = Item.from_v2_xml(response) else item = Item.from_xml(response) end # Store connection in object for future use item.connection = connection # Done return item end def self.get(connection, path, options = {}) unless options.is_a?(Hash) raise AMEE::ArgumentError.new("Third argument must be a hash of options!") end # Convert to AMEE options if options[:start_date] && category.connection.version < 2 options[:profileDate] = options[:start_date].amee1_month elsif options[:start_date] && category.connection.version >= 2 options[:startDate] = options[:start_date].xmlschema end options.delete(:start_date) if options[:end_date] && category.connection.version >= 2 options[:endDate] = options[:end_date].xmlschema end options.delete(:end_date) if options[:duration] && category.connection.version >= 2 options[:duration] = "PT#{options[:duration] * 86400}S" end # Load data from path response = connection.get(path, options).body return Item.parse(connection, response) rescue raise AMEE::BadData.new("Couldn't load ProfileItem. Check that your URL is correct.\n#{response}") end def self.create(category, data_item_uid, options = {}) create_without_category(category.connection, category.full_path, data_item_uid, options) end def self.create_without_category(connection, path, data_item_uid, options = {}) # Do we want to automatically fetch the item afterwards? get_item = options.delete(:get_item) get_item = true if get_item.nil? # Store format if set format = options[:format] unless options.is_a?(Hash) raise AMEE::ArgumentError.new("Third argument must be a hash of options!") end # Set dates if options[:start_date] && connection.version < 2 options[:validFrom] = options[:start_date].amee1_date elsif options[:start_date] && connection.version >= 2 options[:startDate] = options[:start_date].xmlschema end if options[:end_date] && connection.version >= 2 options[:endDate] = options[:end_date].xmlschema end if options[:duration] && connection.version >= 2 options[:duration] = "PT#{options[:duration] * 86400}S" end # Send data to path options.merge!(:representation => 'full') if (connection.version >= 2) && (get_item == true) options.merge! :dataItemUid => data_item_uid # POST response = connection.post(path, options) # Parse response category = response.body.empty? ? nil : Category.parse(connection, response.body, options) if response['Location'] location = response['Location'].match("https??://.*?(/.*)")[1] else location = category.full_path + "/" + category.items[0][:path] end if get_item == true if connection.version >= 2 item = category.items.first item[:connection] = category.connection item[:profile_uid] = category.profile_uid item[:data_item_label] = item.delete(:dataItemLabel) item[:data_item_uid] = item.delete(:dataItemUid) item[:start_date] = item.delete(:startDate) item[:end_date] = item.delete(:endDate) item[:total_amount] = item.delete(:amount) || item.delete(:amountPerMonth) item[:total_amount_unit] = item.delete(:amount_unit) || "kg/month" values = [] item[:values].each do |k,v| values << v.merge(:path => k.to_s) end item[:values] = values return AMEE::Profile::Item.new(item) else get_options = {} get_options[:returnUnit] = options[:returnUnit] if options[:returnUnit] get_options[:returnPerUnit] = options[:returnPerUnit] if options[:returnPerUnit] get_options[:format] = format if format return AMEE::Profile::Item.get(connection, location, get_options) end else return location end rescue raise AMEE::BadData.new("Couldn't create ProfileItem. Check that your information is correct.\n#{response}") end def self.create_batch(category, items, options = {}) create_batch_without_category(category.connection, category.full_path, items) end def self.create_batch_without_category(connection, category_path, items, options = {}) if connection.format == :json options.merge! :profileItems => items post_data = options.to_json else options.merge!({:ProfileItems => items}) post_data = options.to_xml(:root => "ProfileCategory", :skip_types => true, :skip_nil => true) end # Post to category response = connection.raw_post(category_path, post_data).body # Send back a category object containing all the created items unless response.empty? return AMEE::Profile::Category.parse_batch(connection, response) else return true end end def self.update(connection, path, options = {}) # Do we want to automatically fetch the item afterwards? get_item = options.delete(:get_item) get_item = true if get_item.nil? # Set dates if options[:start_date] && connection.version < 2 options[:validFrom] = options[:start_date].amee1_date elsif options[:start_date] && connection.version >= 2 options[:startDate] = options[:start_date].xmlschema end options.delete(:start_date) if options[:end_date] && connection.version >= 2 options[:endDate] = options[:end_date].xmlschema end options.delete(:end_date) if options[:duration] && connection.version >= 2 options[:duration] = "PT#{options[:duration] * 86400}S" end # Go response = connection.put(path, options) if get_item if response.body.empty? return Item.get(connection, path) else return Item.parse(connection, response.body) end end rescue raise AMEE::BadData.new("Couldn't update ProfileItem. Check that your information is correct.\n#{response}") end def update(options = {}) AMEE::Profile::Item.update(connection, full_path, options) end def self.update_batch(category, items) update_batch_without_category(category.connection, category.full_path, items) end def self.update_batch_without_category(connection, category_path, items) if connection.format == :json put_data = ({:profileItems => items}).to_json else put_data = ({:ProfileItems => items}).to_xml(:root => "ProfileCategory", :skip_types => true, :skip_nil => true) end # Post to category response = connection.raw_put(category_path, put_data).body # Send back a category object containing all the created items unless response.empty? return AMEE::Profile::Category.parse(connection, response, nil) else return true end end def self.delete(connection, path) connection.delete(path) rescue raise AMEE::BadData.new("Couldn't delete ProfileItem. Check that your information is correct.") end def value(name_or_path) val = values.find{ |x| x[:name] == name_or_path || x[:path] == name_or_path} val ? val[:value] : nil end end end end