Repository URL to install this package:
|
Version:
2.0.7.2 ▾
|
# For more information on what the possible values of fields that are passed to the RETS server can be, see {http://www.rets.org/documentation}.
module RETS
module Base
class Core
GET_OBJECT_DATA = ["object-id", "description", "content-id", "content-description", "location", "content-type", "preferred"]
# Can be called after any {RETS::Base::Core} call that hits the RETS Server.
# @return [String] How big the request was
attr_reader :request_size
# Can be called after any {RETS::Base::Core} call that hits the RETS Server.
# @return [String] SHA1 hash of the request
attr_reader :request_hash
# Can be called after any {#get_object} or {#search} call that hits the RETS Server.
# @return [Float] How long the request took
attr_reader :request_time
# Can be called after any {RETS::Base::Core} call that hits the RETS Server.
# @return [Hash]
# Gives access to the miscellaneous RETS data, such as reply text, code, delimiter, count and so on depending on the API call made.
# * *text* (String) - Reply text from the server
# * *code* (String) - Reply code from the server
attr_reader :rets_data
def initialize(http, urls)
@http = http
@urls = urls
end
##
# Attempts to logout of the RETS server.
#
# @raise [RETS::CapabilityNotFound]
# @raise [RETS::APIError]
# @raise [RETS::HTTPError]
def logout
unless @urls[:logout]
raise RETS::CapabilityNotFound.new("No Logout capability found for given user.")
end
@http.request(:url => @urls[:logout])
nil
end
##
# Whether the RETS server has the requested capability.
#
# @param [Symbol] type Lowercase of the capability, "getmetadata", "getobject" and so on
# @return [Boolean]
def has_capability?(type)
@urls.has_key?(type)
end
##
# Requests metadata from the RETS server.
#
# @param [Hash] args
# @option args [String] :type Metadata to request, the same value if you were manually making the request, "METADATA-SYSTEM", "METADATA-CLASS" and so on
# @option args [String] :id Filter the data returned by ID, "*" would return all available data
# @option args [Integer, Optional] :read_timeout How many seconds to wait before giving up
# @option args [Integer, Optional] :open_timeout How many seconds to wait before giving up to connect
# @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
#
# @yield For every group of metadata downloaded
# @yieldparam [String] :type Type of data that was parsed with "METADATA-" stripped out, for "METADATA-SYSTEM" this will be "SYSTEM"
# @yieldparam [Hash] :attrs Attributes of the data, generally *Version*, *Date* and *Resource* but can vary depending on what metadata you requested
# @yieldparam [Array] :metadata Array of hashes with metadata info
#
# @raise [RETS::CapabilityNotFound]
# @raise [RETS::APIError]
# @raise [RETS::HTTPError]
# @see #rets_data
# @see #request_size
# @see #request_hash
def get_metadata(args, &block)
raise ArgumentError, "No block passed" unless block_given?
unless @urls[:getmetadata]
raise RETS::CapabilityNotFound.new("No GetMetadata capability found for given user.")
end
@request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
start = Time.now.utc.to_f
@http.request(:url => @urls[:getmetadata], :open_timeout => args[:open_timeout], :read_timeout => args[:read_timeout], :disable_compression => !args[:disable_stream], :params => {:Format => :COMPACT, :Type => args[:type], :ID => args[:id]}) do |response|
if args[:disable_stream]
stream = StringIO.new(response.body)
@request_time = Time.now.utc.to_f - start
else
stream = RETS::StreamHTTP.new(response)
end
sax = RETS::Base::SAXMetadata.new(block)
Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
if args[:disable_stream]
@request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
else
@request_size, @request_hash = stream.size, stream.hash
end
@rets_data = sax.rets_data
end
nil
end
##
# Requests an object from the RETS server.
#
# @param [Hash] args
# @option args [String] :resource Resource to load, typically *Property*
# @option args [String] :type Type of object you want, usually *Photo*
# @option args [String] :id What objects to return
# @option args [Array, Optional] :accept Array of MIME types to accept, by default this is *image/png*, *image/gif* and *image/jpeg*
# @option args [Boolean, Optional] :location Return the location of the object rather than the contents of it
# @option args [Integer, Optional] :read_timeout How many seconds to wait before timing out
# @option args [Integer, Optional] :open_timeout How many seconds to wait before giving up to connect
#
# @yield For every object downloaded
# @yieldparam [Hash] :headers Object headers
# * *object-id* (String) - Objects ID
# * *content-id* (String) - Content ID
# * *content-type* (String) - MIME type of the content
# * *description* (String, Optional) - A description of the object
# * *location* (String, Optional) - Where the file is located, only returned is *location* is true
# @yieldparam [String, Optional] :content Content for the object, not called when *location* is set
#
# @raise [RETS::CapabilityNotFound]
# @raise [RETS::APIError]
# @raise [RETS::HTTPError]
# @see #rets_data
# @see #request_size
# @see #request_hash
def get_object(args, &block)
raise ArgumentError, "No block passed" unless block_given?
unless @urls[:getobject]
raise RETS::CapabilityNotFound.new("No GetObject capability found for given user.")
end
req = {:url => @urls[:getobject], :open_timeout => args[:open_timeout], :read_timeout => args[:read_timeout], :headers => {}}
req[:params] = {:Resource => args[:resource], :Type => args[:type], :Location => (args[:location] ? 1 : 0), :ID => args[:id]}
if args[:accept].is_a?(Array)
req[:headers]["Accept"] = args[:accept].join(",")
else
req[:headers]["Accept"] = "image/png,image/gif,image/jpeg"
end
# Will get swapped to a streaming call rather than a download-and-parse later, easy to do as it's called with a block now
start = Time.now.utc.to_f
@request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
@http.request(req) do |response|
body = response.read_body
@request_time = Time.now.utc.to_f - start
@request_size, @request_hash = body.length, Digest::SHA1.hexdigest(body)
# Make sure we aren't erroring
if body =~ /(<RETS (.+)\>)/
# RETSIQ (and probably others) return a <RETS> tag on a location request without any error inside
# since parsing errors out of full image data calls is a tricky pain. We're going to keep the
# loose error checking, but will confirm that it has an actual error code
code, text = @http.get_rets_response(Nokogiri::XML($1).xpath("//RETS").first)
unless code == "0"
@rets_data = {:code => code, :text => text}
if code == "20403"
return
else
raise RETS::APIError.new("#{code}: #{text}", code, text)
end
end
end
# Using a wildcard somewhere
if response.content_type == "multipart/parallel"
boundary = response.type_params["boundary"]
boundary.gsub!(/^"|"$/, "")
parts = body.split("--#{boundary}\r\n")
parts.last.gsub!("\r\n--#{boundary}--", "")
parts.each do |part|
part.strip!
next if part == ""
headers, content = part.split("\r\n\r\n", 2)
parsed_headers = {}
headers.split("\r\n").each do |line|
name, value = line.split(":", 2)
next unless value and value != ""
parsed_headers[name.downcase] = value.strip
end
# Check off the first children because some Rap Rets seems to use RETS-Status
# and it will include it with an object while returning actual data.
# It only does this for multipart requests, single image pulls will use <RETS> like it should.
if parsed_headers["content-type"] == "text/xml"
code, text = @http.get_rets_response(Nokogiri::XML(content).children.first)
next if code == "20403"
end
if block.arity == 1
yield parsed_headers
else
yield parsed_headers, content
end
end
# Either text (error) or an image of some sorts, which is irrelevant for this
else
headers = {}
GET_OBJECT_DATA.each do |field|
next unless response.header[field] and response.header[field] != ""
headers[field] = response.header[field].strip
end
if block.arity == 1
yield headers
else
yield headers, body
end
end
end
nil
end
##
# Searches the RETS server for data.
#
# @param [Hash] args
# @option args [String] :search_type What to search on, typically *Property*, *Office* or *Agent*
# @option args [String] :class What class of data to return, varies between RETS implementations and can be anything from *1* to *ResidentialProperty*
# @option args [String] :query How to filter data, should be unescaped as CGI::escape will be called on the string
# @option args [Symbol, Optional] :count_mode Either *:only* to return just the total records found or *:both* to get count and records returned
# @option args [Integer, Optional] :limit Limit total records returned
# @option args [Integer, Optional] :offset Offset to start returning records from
# @option args [Array, Optional] :select Restrict the fields the RETS server returns
# @option args [Boolean, Optional] :standard_names Whether to use standard names for all fields
# @option args [String, Optional] :restricted String to show in place of a field value for any restricted fields the user cannot see
# @option args [Integer, Optional] :read_timeout How long to wait for data from the socket before giving up
# @option args [Integer, Optional] :open_timeout How many seconds to wait before giving up to connect
# @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
#
# @yield Called for every <DATA></DATA> group from the RETS server
# @yieldparam [Hash] :data One record of data from the RETS server
#
# @raise [RETS::CapabilityNotFound]
# @raise [RETS::APIError]
# @raise [RETS::HTTPError]
# @see #rets_data
# @see #request_size
# @see #request_hash
def search(args, &block)
if !block_given? and args[:count_mode] != :only
raise ArgumentError, "No block found"
end
unless @urls[:search]
raise RETS::CapabilityNotFound.new("Cannot find URL for Search call")
end
req = {:url => @urls[:search], :open_timeout => args[:open_timeout], :read_timeout => args[:read_timeout], :disable_compression => !args[:disable_stream]}
req[:params] = {:Format => "COMPACT-DECODED", :SearchType => args[:search_type], :QueryType => "DMQL2", :Query => args[:query], :Class => args[:class], :Limit => args[:limit], :Offset => args[:offset], :RestrictedIndicator => args[:restricted]}
req[:params][:Select] = args[:select].join(",") if args[:select].is_a?(Array)
req[:params][:StandardNames] = 1 if args[:standard_names]
if args[:count_mode] == :only
req[:params][:Count] = 2
elsif args[:count_mode] == :both
req[:params][:Count] = 1
end
@request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, {}
start = Time.now.utc.to_f
@http.request(req) do |response|
if args[:disable_stream]
stream = StringIO.new(response.body)
@request_time = Time.now.utc.to_f - start
else
stream = RETS::StreamHTTP.new(response)
end
sax = RETS::Base::SAXSearch.new(@rets_data, block)
Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
if args[:disable_stream]
@request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
else
@request_size, @request_hash = stream.size, stream.hash
end
end
nil
end
end
end
end