rsolr 1.1.2 → 2.3.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -13
- data/CHANGES.txt +11 -0
- data/Gemfile +0 -4
- data/README.rdoc +26 -9
- data/Rakefile +16 -3
- data/lib/rsolr.rb +38 -12
- data/lib/rsolr/char.rb +3 -21
- data/lib/rsolr/client.rb +75 -75
- data/lib/rsolr/document.rb +59 -0
- data/lib/rsolr/field.rb +87 -0
- data/lib/rsolr/generator.rb +5 -0
- data/lib/rsolr/json.rb +60 -0
- data/lib/rsolr/response.rb +2 -2
- data/lib/rsolr/uri.rb +2 -52
- data/lib/rsolr/version.rb +1 -1
- data/lib/rsolr/xml.rb +38 -107
- data/rsolr.gemspec +7 -3
- data/spec/api/client_spec.rb +90 -92
- data/spec/api/document_spec.rb +48 -0
- data/spec/api/error_spec.rb +2 -1
- data/spec/api/json_spec.rb +198 -0
- data/spec/api/pagination_spec.rb +3 -9
- data/spec/api/rsolr_spec.rb +3 -11
- data/spec/api/uri_spec.rb +2 -93
- data/spec/api/xml_spec.rb +45 -11
- data/spec/integration/solr5_spec.rb +9 -1
- data/spec/spec_helper.rb +88 -2
- metadata +44 -15
- data/lib/rsolr/connection.rb +0 -74
- data/spec/api/char_spec.rb +0 -23
- data/spec/api/connection_spec.rb +0 -140
- data/tasks/rdoc.rake +0 -11
- data/tasks/rsolr.rake +0 -10
- data/tasks/spec.rake +0 -2
@@ -0,0 +1,59 @@
|
|
1
|
+
module RSolr
|
2
|
+
class Document
|
3
|
+
CHILD_DOCUMENT_KEY = '_childDocuments_'.freeze
|
4
|
+
|
5
|
+
# "attrs" is a hash for setting the "doc" xml attributes
|
6
|
+
# "fields" is an array of Field objects
|
7
|
+
attr_accessor :attrs, :fields
|
8
|
+
|
9
|
+
# "doc_hash" must be a Hash/Mash object
|
10
|
+
# If a value in the "doc_hash" is an array,
|
11
|
+
# a field object is created for each value...
|
12
|
+
def initialize(doc_hash = {})
|
13
|
+
@fields = []
|
14
|
+
doc_hash.each_pair do |field, values|
|
15
|
+
add_field(field, values)
|
16
|
+
end
|
17
|
+
@attrs={}
|
18
|
+
end
|
19
|
+
|
20
|
+
# returns an array of fields that match the "name" arg
|
21
|
+
def fields_by_name(name)
|
22
|
+
@fields.select{|f|f.name==name}
|
23
|
+
end
|
24
|
+
|
25
|
+
# returns the *first* field that matches the "name" arg
|
26
|
+
def field_by_name(name)
|
27
|
+
@fields.detect{|f|f.name==name}
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Add a field value to the document. Options map directly to
|
32
|
+
# XML attributes in the Solr <field> node.
|
33
|
+
# See http://wiki.apache.org/solr/UpdateXmlMessages#head-8315b8028923d028950ff750a57ee22cbf7977c6
|
34
|
+
#
|
35
|
+
# === Example:
|
36
|
+
#
|
37
|
+
# document.add_field('title', 'A Title', :boost => 2.0)
|
38
|
+
#
|
39
|
+
def add_field(name, values, options = {})
|
40
|
+
RSolr::Array.wrap(values).each do |v|
|
41
|
+
field_attrs = { name: name }
|
42
|
+
field_attrs[:type] = DocumentField if name.to_s == CHILD_DOCUMENT_KEY
|
43
|
+
|
44
|
+
@fields << RSolr::Field.instance(options.merge(field_attrs), v)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def as_json
|
49
|
+
@fields.group_by(&:name).each_with_object({}) do |(field, values), result|
|
50
|
+
v = values.map(&:as_json)
|
51
|
+
if v.length > 1 && v.first.is_a?(Hash) && v.first.key?(:value)
|
52
|
+
v = v.first.merge(value: v.map { |single| single[:value] })
|
53
|
+
end
|
54
|
+
v = v.first if v.length == 1 && field.to_s != CHILD_DOCUMENT_KEY
|
55
|
+
result[field] = v
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/rsolr/field.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module RSolr
|
2
|
+
class Field
|
3
|
+
def self.instance(attrs, value)
|
4
|
+
attrs = attrs.dup
|
5
|
+
field_type = attrs.delete(:type) { value.class.name }
|
6
|
+
|
7
|
+
klass = if field_type.is_a? String
|
8
|
+
class_for_field(field_type)
|
9
|
+
elsif field_type.is_a? Class
|
10
|
+
field_type
|
11
|
+
else
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
klass.new(attrs, value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.class_for_field(field_type)
|
19
|
+
potential_class_name = field_type + 'Field'.freeze
|
20
|
+
search_scope = Module.nesting[1]
|
21
|
+
search_scope.const_defined?(potential_class_name, false) ? search_scope.const_get(potential_class_name) : self
|
22
|
+
end
|
23
|
+
private_class_method :class_for_field
|
24
|
+
|
25
|
+
# "attrs" is a hash for setting the "doc" xml attributes
|
26
|
+
# "value" is the text value for the node
|
27
|
+
attr_accessor :attrs, :source_value
|
28
|
+
|
29
|
+
# "attrs" must be a hash
|
30
|
+
# "value" should be something that responds to #_to_s
|
31
|
+
def initialize(attrs, source_value)
|
32
|
+
@attrs = attrs
|
33
|
+
@source_value = source_value
|
34
|
+
end
|
35
|
+
|
36
|
+
# the value of the "name" attribute
|
37
|
+
def name
|
38
|
+
attrs[:name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def value
|
42
|
+
source_value
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json
|
46
|
+
if attrs[:update]
|
47
|
+
{ attrs[:update] => value }
|
48
|
+
elsif attrs.any? { |k, _| k != :name }
|
49
|
+
hash = attrs.dup
|
50
|
+
hash.delete(:name)
|
51
|
+
hash.merge(value: value)
|
52
|
+
else
|
53
|
+
value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class DateField < Field
|
59
|
+
def value
|
60
|
+
Time.utc(source_value.year, source_value.mon, source_value.mday).iso8601
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TimeField < Field
|
65
|
+
def value
|
66
|
+
source_value.getutc.strftime('%FT%TZ')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class DateTimeField < Field
|
71
|
+
def value
|
72
|
+
source_value.to_time.getutc.iso8601
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class DocumentField < Field
|
77
|
+
def value
|
78
|
+
return RSolr::Document.new(source_value) if source_value.respond_to? :each_pair
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def as_json
|
84
|
+
value.as_json
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/rsolr/json.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSolr::JSON
|
4
|
+
class Generator < RSolr::Generator
|
5
|
+
CONTENT_TYPE = 'application/json'.freeze
|
6
|
+
|
7
|
+
def content_type
|
8
|
+
CONTENT_TYPE
|
9
|
+
end
|
10
|
+
|
11
|
+
def add data, add_attrs = {}
|
12
|
+
add_attrs ||= {}
|
13
|
+
data = RSolr::Array.wrap(data)
|
14
|
+
|
15
|
+
if add_attrs.empty? && data.none? { |doc| doc.is_a?(RSolr::Document) && !doc.attrs.empty? }
|
16
|
+
data.map do |doc|
|
17
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
18
|
+
yield doc if block_given?
|
19
|
+
doc.as_json
|
20
|
+
end.to_json
|
21
|
+
else
|
22
|
+
i = 0
|
23
|
+
data.each_with_object({}) do |doc, hash|
|
24
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
25
|
+
yield doc if block_given?
|
26
|
+
hash["add__UNIQUE_RSOLR_SUFFIX_#{i += 1}"] = add_attrs.merge(doc.attrs).merge(doc: doc.as_json)
|
27
|
+
end.to_json.gsub(/__UNIQUE_RSOLR_SUFFIX_\d+/, '')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# generates a commit message
|
32
|
+
def commit(opts = {})
|
33
|
+
opts ||= {}
|
34
|
+
{ commit: opts }.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
# generates a optimize message
|
38
|
+
def optimize(opts = {})
|
39
|
+
opts ||= {}
|
40
|
+
{ optimize: opts }.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
# generates a rollback message
|
44
|
+
def rollback
|
45
|
+
{ rollback: {} }.to_json
|
46
|
+
end
|
47
|
+
|
48
|
+
# generates a delete message
|
49
|
+
# "ids" can be a single value or array of values
|
50
|
+
def delete_by_id(ids)
|
51
|
+
{ delete: ids }.to_json
|
52
|
+
end
|
53
|
+
|
54
|
+
# generates a delete message
|
55
|
+
# "queries" can be a single value or an array of values
|
56
|
+
def delete_by_query(queries)
|
57
|
+
{ delete: { query: queries } }.to_json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/rsolr/response.rb
CHANGED
@@ -11,7 +11,7 @@ module RSolr::Response
|
|
11
11
|
@request = request
|
12
12
|
@response = response
|
13
13
|
self.merge!(result)
|
14
|
-
if self["response"] && self["response"]["docs"].is_a?(Array)
|
14
|
+
if self["response"] && self["response"]["docs"].is_a?(::Array)
|
15
15
|
docs = PaginatedDocSet.new(self["response"]["docs"])
|
16
16
|
docs.per_page = request[:params]["rows"]
|
17
17
|
docs.page_start = request[:params]["start"]
|
@@ -41,7 +41,7 @@ module RSolr::Response
|
|
41
41
|
end
|
42
42
|
|
43
43
|
# A response module which gets mixed into the solr ["response"]["docs"] array.
|
44
|
-
class PaginatedDocSet < Array
|
44
|
+
class PaginatedDocSet < ::Array
|
45
45
|
|
46
46
|
attr_accessor :page_start, :per_page, :page_total
|
47
47
|
if not (Object.const_defined?("RUBY_ENGINE") and Object::RUBY_ENGINE=='rbx')
|
data/lib/rsolr/uri.rb
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
require 'uri'
|
2
2
|
|
3
3
|
module RSolr::Uri
|
4
|
-
|
5
|
-
def create url
|
6
|
-
::URI.parse (url[-1] == '/' || URI.parse(url).query) ? url : "#{url}/"
|
7
|
-
end
|
8
|
-
|
9
4
|
# Creates a Solr based query string.
|
10
5
|
# Keys that have arrays values are set multiple times:
|
11
6
|
# params_to_solr(:q => 'query', :fq => ['a', 'b'])
|
@@ -13,13 +8,13 @@ module RSolr::Uri
|
|
13
8
|
# ?q=query&fq=a&fq=b
|
14
9
|
# @param [boolean] escape false if no URI escaping is to be performed. Default true.
|
15
10
|
# @return [String] Solr query params as a String, suitable for use in a url
|
16
|
-
def params_to_solr(params, escape = true)
|
11
|
+
def self.params_to_solr(params, escape = true)
|
17
12
|
return URI.encode_www_form(params.reject{|k,v| k.to_s.empty? || v.to_s.empty?}) if escape
|
18
13
|
|
19
14
|
# escape = false if we are here
|
20
15
|
mapped = params.map do |k, v|
|
21
16
|
next if v.to_s.empty?
|
22
|
-
if v.class == Array
|
17
|
+
if v.class == ::Array
|
23
18
|
params_to_solr(v.map { |x| [k, x] }, false)
|
24
19
|
else
|
25
20
|
"#{k}=#{v}"
|
@@ -27,49 +22,4 @@ module RSolr::Uri
|
|
27
22
|
end
|
28
23
|
mapped.compact.join("&")
|
29
24
|
end
|
30
|
-
|
31
|
-
# Returns a query string param pair as a string.
|
32
|
-
# Both key and value are URI escaped, unless third param is false
|
33
|
-
# @param [boolean] escape false if no URI escaping is to be performed. Default true.
|
34
|
-
# @deprecated - used to be called from params_to_solr before 2015-02-25
|
35
|
-
def build_param(k, v, escape = true)
|
36
|
-
warn "[DEPRECATION] `RSolr::Uri.build_param` is deprecated. Use `URI.encode_www_form_component` or k=v instead."
|
37
|
-
escape ?
|
38
|
-
"#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" :
|
39
|
-
"#{k}=#{v}"
|
40
|
-
end
|
41
|
-
|
42
|
-
# 2015-02 Deprecated: use URI.encode_www_form_component(s)
|
43
|
-
#
|
44
|
-
# Performs URI escaping so that you can construct proper
|
45
|
-
# query strings faster. Use this rather than the cgi.rb
|
46
|
-
# version since it's faster.
|
47
|
-
# (Stolen from Rack).
|
48
|
-
# http://www.rubydoc.info/github/rack/rack/URI.encode_www_form_component
|
49
|
-
# @deprecated
|
50
|
-
def escape_query_value(s)
|
51
|
-
warn "[DEPRECATION] `RSolr::Uri.escape_query_value` is deprecated. Use `URI.encode_www_form_component` instead."
|
52
|
-
URI.encode_www_form_component(s)
|
53
|
-
# s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/u) {
|
54
|
-
# '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
55
|
-
# }.tr(' ', '+')
|
56
|
-
end
|
57
|
-
|
58
|
-
# Return the bytesize of String; uses String#size under Ruby 1.8 and
|
59
|
-
# String#bytesize under 1.9.
|
60
|
-
# @deprecated as bytesize was only used by escape_query_value which is itself deprecated
|
61
|
-
if ''.respond_to?(:bytesize)
|
62
|
-
def bytesize(string)
|
63
|
-
warn "[DEPRECATION] `RSolr::Uri.bytesize` is deprecated. Use String.bytesize"
|
64
|
-
string.bytesize
|
65
|
-
end
|
66
|
-
else
|
67
|
-
def bytesize(string)
|
68
|
-
warn "[DEPRECATION] `RSolr::Uri.bytesize` is deprecated. Use String.size"
|
69
|
-
string.size
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
extend self
|
74
|
-
|
75
25
|
end
|
data/lib/rsolr/version.rb
CHANGED
data/lib/rsolr/xml.rb
CHANGED
@@ -1,102 +1,8 @@
|
|
1
|
-
begin; require 'nokogiri'; rescue LoadError; end
|
2
|
-
require 'time'
|
3
|
-
|
4
1
|
module RSolr::Xml
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# "attrs" is a hash for setting the "doc" xml attributes
|
9
|
-
# "fields" is an array of Field objects
|
10
|
-
attr_accessor :attrs, :fields
|
11
|
-
|
12
|
-
# "doc_hash" must be a Hash/Mash object
|
13
|
-
# If a value in the "doc_hash" is an array,
|
14
|
-
# a field object is created for each value...
|
15
|
-
def initialize(doc_hash = {})
|
16
|
-
@fields = []
|
17
|
-
doc_hash.each_pair do |field,values|
|
18
|
-
# create a new field for each value (multi-valued)
|
19
|
-
wrap(values).each do |v|
|
20
|
-
v = format_value(v)
|
21
|
-
next if v.empty?
|
22
|
-
@fields << RSolr::Xml::Field.new({:name=>field}, v)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
@attrs={}
|
26
|
-
end
|
27
|
-
|
28
|
-
# returns an array of fields that match the "name" arg
|
29
|
-
def fields_by_name(name)
|
30
|
-
@fields.select{|f|f.name==name}
|
31
|
-
end
|
32
|
-
|
33
|
-
# returns the *first* field that matches the "name" arg
|
34
|
-
def field_by_name(name)
|
35
|
-
@fields.detect{|f|f.name==name}
|
36
|
-
end
|
37
|
-
|
38
|
-
#
|
39
|
-
# Add a field value to the document. Options map directly to
|
40
|
-
# XML attributes in the Solr <field> node.
|
41
|
-
# See http://wiki.apache.org/solr/UpdateXmlMessages#head-8315b8028923d028950ff750a57ee22cbf7977c6
|
42
|
-
#
|
43
|
-
# === Example:
|
44
|
-
#
|
45
|
-
# document.add_field('title', 'A Title', :boost => 2.0)
|
46
|
-
#
|
47
|
-
def add_field(name, value, options = {})
|
48
|
-
@fields << RSolr::Xml::Field.new(options.merge({:name=>name}), value)
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def format_value(v)
|
54
|
-
case v
|
55
|
-
when Time
|
56
|
-
v.getutc.iso8601
|
57
|
-
when DateTime
|
58
|
-
v.to_time.getutc.iso8601
|
59
|
-
when Date
|
60
|
-
Time.utc(v.year, v.mon, v.mday).iso8601
|
61
|
-
else
|
62
|
-
v.to_s
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def wrap(object)
|
67
|
-
if object.nil?
|
68
|
-
[]
|
69
|
-
elsif object.respond_to?(:to_ary)
|
70
|
-
object.to_ary || [object]
|
71
|
-
elsif object.is_a? Enumerable
|
72
|
-
object
|
73
|
-
else
|
74
|
-
[object]
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
2
|
+
Document = RSolr::Document
|
3
|
+
Field = RSolr::Field
|
78
4
|
|
79
|
-
class
|
80
|
-
|
81
|
-
# "attrs" is a hash for setting the "doc" xml attributes
|
82
|
-
# "value" is the text value for the node
|
83
|
-
attr_accessor :attrs, :value
|
84
|
-
|
85
|
-
# "attrs" must be a hash
|
86
|
-
# "value" should be something that responds to #_to_s
|
87
|
-
def initialize(attrs, value)
|
88
|
-
@attrs = attrs
|
89
|
-
@value = value
|
90
|
-
end
|
91
|
-
|
92
|
-
# the value of the "name" attribute
|
93
|
-
def name
|
94
|
-
@attrs[:name]
|
95
|
-
end
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
class Generator
|
5
|
+
class Generator < RSolr::Generator
|
100
6
|
class << self
|
101
7
|
attr_accessor :use_nokogiri
|
102
8
|
|
@@ -110,7 +16,14 @@ module RSolr::Xml
|
|
110
16
|
end
|
111
17
|
end
|
112
18
|
end
|
113
|
-
self.use_nokogiri =
|
19
|
+
self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false
|
20
|
+
|
21
|
+
CONTENT_TYPE = 'text/xml'.freeze
|
22
|
+
|
23
|
+
def content_type
|
24
|
+
CONTENT_TYPE
|
25
|
+
end
|
26
|
+
|
114
27
|
|
115
28
|
def nokogiri_build &block
|
116
29
|
b = ::Nokogiri::XML::Builder.new do |xml|
|
@@ -158,17 +71,13 @@ module RSolr::Xml
|
|
158
71
|
#
|
159
72
|
def add data, add_attrs = nil, &block
|
160
73
|
add_attrs ||= {}
|
161
|
-
data =
|
74
|
+
data = RSolr::Array.wrap(data)
|
162
75
|
build do |xml|
|
163
76
|
xml.add(add_attrs) do |add_node|
|
164
77
|
data.each do |doc|
|
165
|
-
doc = RSolr::
|
78
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
166
79
|
yield doc if block_given?
|
167
|
-
doc_node_builder =
|
168
|
-
doc.fields.each do |field_obj|
|
169
|
-
doc_node.field field_obj.value, field_obj.attrs
|
170
|
-
end
|
171
|
-
end
|
80
|
+
doc_node_builder = to_xml(doc)
|
172
81
|
self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
|
173
82
|
end
|
174
83
|
end
|
@@ -195,7 +104,7 @@ module RSolr::Xml
|
|
195
104
|
# generates a <delete><id>ID</id></delete> message
|
196
105
|
# "ids" can be a single value or array of values
|
197
106
|
def delete_by_id ids
|
198
|
-
ids =
|
107
|
+
ids = RSolr::Array.wrap(ids)
|
199
108
|
build do |xml|
|
200
109
|
xml.delete do |delete_node|
|
201
110
|
ids.each do |id|
|
@@ -208,12 +117,34 @@ module RSolr::Xml
|
|
208
117
|
# generates a <delete><query>ID</query></delete> message
|
209
118
|
# "queries" can be a single value or an array of values
|
210
119
|
def delete_by_query(queries)
|
211
|
-
queries =
|
120
|
+
queries = RSolr::Array.wrap(queries)
|
212
121
|
build do |xml|
|
213
122
|
xml.delete do |delete_node|
|
214
123
|
queries.each { |query| delete_node.query(query) }
|
215
124
|
end
|
216
125
|
end
|
217
126
|
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def to_xml(doc)
|
131
|
+
lambda do |doc_node|
|
132
|
+
doc.fields.each do |field_obj|
|
133
|
+
value = field_obj.value
|
134
|
+
|
135
|
+
if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
|
136
|
+
child_node_builder = to_xml(field_obj.value)
|
137
|
+
self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
|
138
|
+
elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
|
139
|
+
update_attr, real_value = value.first
|
140
|
+
doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
|
141
|
+
elsif value.nil?
|
142
|
+
doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
|
143
|
+
else
|
144
|
+
doc_node.field field_obj.value, field_obj.attrs
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
218
149
|
end
|
219
150
|
end
|