lib/rsolr/xml.rb



module RSolr::Xml
  Document = RSolr::Document
  Field = RSolr::Field

  class Generator < RSolr::Generator
    class << self
      attr_accessor :use_nokogiri

      def builder_proc
        if use_nokogiri 
          require 'nokogiri' unless defined?(::Nokogiri::XML::Builder)
          :nokogiri_build
        else
          require 'builder' unless defined?(::Builder::XmlMarkup)
          :builder_build
        end
      end
    end
    self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false

    CONTENT_TYPE = 'text/xml'.freeze

    def content_type
      CONTENT_TYPE
    end


    def nokogiri_build &block
      b = ::Nokogiri::XML::Builder.new do |xml|
        block_given? ? yield(xml) : xml
      end
      '<?xml version="1.0" encoding="UTF-8"?>'+b.to_xml(:indent => 0, :encoding => 'UTF-8', :save_with => ::Nokogiri::XML::Node::SaveOptions::AS_XML | ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
    end
    protected :nokogiri_build
    
    def builder_build &block
      b = ::Builder::XmlMarkup.new(:indent => 0, :margin => 0, :encoding => 'UTF-8')
      b.instruct!
      block_given? ? yield(b) : b
    end
    protected :builder_build
    
    def build &block
      self.send(self.class.builder_proc,&block)
    end
    
    # generates "add" xml for updating solr
    # "data" can be a hash or an array of hashes.
    # - each hash should be a simple key=>value pair representing a solr doc.
    # If a value is an array, multiple fields will be created.
    #
    # "add_attrs" can be a hash for setting the add xml element attributes.
    # 
    # This method can also accept a block.
    # The value yielded to the block is a Message::Document; for each solr doc in "data".
    # You can set xml element attributes for each "doc" element or individual "field" elements.
    #
    # For example:
    #
    # solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
    #   doc_msg.attrs[:boost] = 10.00 # boost the document
    #   nickname = doc_msg.field_by_name(:nickname)
    #   nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
    # end
    #
    # would result in an add element having the attributes boost="10.0"
    # and a commitWithin="1.0".
    # Each doc element would have a boost="10.0".
    # The "nickname" field would have a boost="20.0"
    # if the doc had a "nickname" field with the value of "Tim".
    #
    def add data, add_attrs = nil, &block
      add_attrs ||= {}
      data = RSolr::Array.wrap(data)
      build do |xml|
        xml.add(add_attrs) do |add_node|
          data.each do |doc|
            doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
            yield doc if block_given?
            doc_node_builder = to_xml(doc)
            self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
          end
        end
      end
    end
    
    # generates a <commit/> message
    def commit opts = nil
      opts ||= {}
      build {|xml| xml.commit(opts) }
    end
    
    # generates a <optimize/> message
    def optimize opts = nil
      opts ||= {}
      build {|xml| xml.optimize(opts) }
    end
    
    # generates a <rollback/> message
    def rollback
      build {|xml| xml.rollback({}) }
    end

    # generates a <delete><id>ID</id></delete> message
    # "ids" can be a single value or array of values
    def delete_by_id ids
      ids = RSolr::Array.wrap(ids)
      build do |xml|
        xml.delete do |delete_node|
          ids.each do |id| 
            self.class.use_nokogiri ? delete_node.id_(id) : delete_node.id(id)
          end
        end
      end
    end

    # generates a <delete><query>ID</query></delete> message
    # "queries" can be a single value or an array of values
    def delete_by_query(queries)
      queries = RSolr::Array.wrap(queries)
      build do |xml|
        xml.delete do |delete_node|
          queries.each { |query| delete_node.query(query) }
        end
      end
    end

    private

    def to_xml(doc)
      lambda do |doc_node|
        doc.fields.each do |field_obj|
          value = field_obj.value

          if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
            child_node_builder = to_xml(field_obj.value)
            self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
          elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
            update_attr, real_value = value.first
            doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
          elsif value.nil?
            doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
          else
            doc_node.field field_obj.value, field_obj.attrs
          end
        end
      end
    end
  end
end