Method: HappyMapper::ClassMethods#parse

Defined in:
lib/happymapper.rb

#parse(xml, options = {}) ⇒ Object

Parameters:

  • xml (Nokogiri::XML::Node, Nokogiri:XML::Document, String)

    the XML contents to convert into Object.

  • options (Hash) (defaults to: {})

    additional information for parsing. :single => true if requesting a single object, otherwise it defaults to retuning an array of multiple items. :xpath information where to start the parsing :namespace is the namespace to use for additional information.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/happymapper.rb', line 208

def parse(xml, options = {})
  
  # create a local copy of the objects namespace value for this parse execution
  namespace = @namespace
  
  # If the XML specified is an Node then we have what we need.
  if xml.is_a?(Nokogiri::XML::Node)
    node = xml
  else
    
    # If xml is an XML document select the root node of the document
    if xml.is_a?(Nokogiri::XML::Document)
      node = xml.root
    else
      
      # Attempt to parse the xml value with Nokogiri XML as a document
      # and select the root element
      
      xml = Nokogiri::XML(xml)
      node = xml.root
    end

    # if the node name is equal to the tag name then the we are parsing the
    # root element and that is important to record so that we can apply
    # the correct xpath on the elements of this document.

    root = node.name == tag_name
  end

  # if any namespaces have been provied then we should capture those and then
  # merge them with any namespaces found on the xml node and merge all that
  # with any namespaces that have been registered on the object
  
  namespaces = options[:namespaces] || {}
  namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
  namespaces = namespaces.merge(@registered_namespaces)
  
  # if a namespace has been provided then set the current namespace to it
  # or set the default namespace to the one defined under 'xmlns' 
  # or set the default namespace to the namespace that matches 'happymapper's

  if options[:namespace]
    namespace = options[:namespace]
  elsif namespaces.has_key?("xmlns")
    namespace ||= DEFAULT_NS
    namespaces[namespace] = namespaces.delete("xmlns")
  elsif namespaces.has_key?(DEFAULT_NS)
    namespace ||= DEFAULT_NS
  end
  
  # from the options grab any nodes present and if none are present then
  # perform the following to find the nodes for the given class 
  
  nodes = options.fetch(:nodes) do
    
    # when at the root use the xpath '/' otherwise use a more gready './/'
    # unless an xpath has been specified, which should overwrite default
    # and finally attach the current namespace if one has been defined
    # 
    
    xpath  = (root ? '/' : './/')
    xpath  = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
    xpath += "#{namespace}:" if namespace

    nodes = []

    # when finding nodes, do it in this order:
    # 1. specified tag
    # 2. name of element
    # 3. tag_name (derived from class name by default)


    [options[:tag], options[:name], tag_name].compact.each do |xpath_ext|
      begin
        nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
      rescue
        break
      end
      break if nodes && !nodes.empty?
    end

    nodes
  end

  # If the :limit option has been specified then we are going to slice
  # our node results by that amount to allow us the ability to deal with
  # a large result set of data.

  limit = options[:in_groups_of] || nodes.size
  
  # If the limit of 0 has been specified then the user obviously wants
  # none of the nodes that we are serving within this batch of nodes.
  
  return [] if limit == 0

  collection = []
  
  nodes.each_slice(limit) do |slice|
    
    part = slice.map do |n|
      obj = new

      attributes.each do |attr|
        obj.send("#{attr.method_name}=",attr.from_xml_node(n, namespace, namespaces))
      end

      elements.each do |elem|
        obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
      end

      if @text_node
        obj.send("#{@text_node.method_name}=",@text_node.from_xml_node(n, namespace, namespaces))
      end
      
      # If the HappyMapper class has the method #xml_value=, 
      # attr_writer :xml_value, or attr_accessor :xml_value then we want to
      # assign the current xml that we just parsed to the xml_value
    
      if obj.respond_to?('xml_value=')
        n.namespaces.each {|name,path| n[name] = path }
        obj.xml_value = n.to_xml
      end
      
      # If the HappyMapper class has the method #xml_content=,
      # attr_write :xml_content, or attr_accessor :xml_content then we want to
      # assign the child xml that we just parsed to the xml_content

      if obj.respond_to?('xml_content=')
        n = n.children if n.respond_to?(:children)
        obj.xml_content = n.to_xml
      end
    
      # collect the object that we have created
      
      obj
    end
    
    # If a block has been provided and the user has requested that the objects
    # be handled in groups then we should yield the slice of the objects to them
    # otherwise continue to lump them together

    if block_given? and options[:in_groups_of]
      yield part
    else
      collection += part
    end
    
  end

  # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
  nodes = nil

  # If the :single option has been specified or we are at the root element
  # then we are going to return the first item in the collection. Otherwise
  # the return response is going to be an entire array of items.

  if options[:single] or root
    collection.first
  else
    collection
  end
end