UnderpantsGnome-sunspot 0.9.1.1
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.
- data/History.txt +39 -0
- data/LICENSE +18 -0
- data/README.rdoc +154 -0
- data/Rakefile +9 -0
- data/TODO +4 -0
- data/VERSION.yml +4 -0
- data/bin/sunspot-configure-solr +46 -0
- data/bin/sunspot-solr +62 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot.rb +470 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/composite_setup.rb +186 -0
- data/lib/sunspot/configuration.rb +38 -0
- data/lib/sunspot/data_extractor.rb +47 -0
- data/lib/sunspot/date_facet.rb +36 -0
- data/lib/sunspot/date_facet_row.rb +17 -0
- data/lib/sunspot/dsl.rb +3 -0
- data/lib/sunspot/dsl/field_query.rb +72 -0
- data/lib/sunspot/dsl/fields.rb +86 -0
- data/lib/sunspot/dsl/query.rb +59 -0
- data/lib/sunspot/dsl/query_facet.rb +31 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/scope.rb +193 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/facet.rb +51 -0
- data/lib/sunspot/facet_row.rb +34 -0
- data/lib/sunspot/field.rb +157 -0
- data/lib/sunspot/field_factory.rb +126 -0
- data/lib/sunspot/indexer.rb +127 -0
- data/lib/sunspot/instantiated_facet.rb +38 -0
- data/lib/sunspot/instantiated_facet_row.rb +12 -0
- data/lib/sunspot/query.rb +190 -0
- data/lib/sunspot/query/base_query.rb +90 -0
- data/lib/sunspot/query/connective.rb +77 -0
- data/lib/sunspot/query/dynamic_query.rb +69 -0
- data/lib/sunspot/query/field_facet.rb +149 -0
- data/lib/sunspot/query/field_query.rb +57 -0
- data/lib/sunspot/query/pagination.rb +39 -0
- data/lib/sunspot/query/query_facet.rb +72 -0
- data/lib/sunspot/query/query_facet_row.rb +19 -0
- data/lib/sunspot/query/restriction.rb +225 -0
- data/lib/sunspot/query/scope.rb +165 -0
- data/lib/sunspot/query/sort.rb +36 -0
- data/lib/sunspot/query/sort_composite.rb +33 -0
- data/lib/sunspot/query_facet.rb +33 -0
- data/lib/sunspot/query_facet_row.rb +21 -0
- data/lib/sunspot/schema.rb +165 -0
- data/lib/sunspot/search.rb +222 -0
- data/lib/sunspot/search/hit.rb +62 -0
- data/lib/sunspot/session.rb +201 -0
- data/lib/sunspot/setup.rb +271 -0
- data/lib/sunspot/type.rb +200 -0
- data/lib/sunspot/util.rb +164 -0
- data/solr/etc/jetty.xml +212 -0
- data/solr/etc/webdefault.xml +379 -0
- data/solr/lib/jetty-6.1.3.jar +0 -0
- data/solr/lib/jetty-util-6.1.3.jar +0 -0
- data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr/solr/conf/elevate.xml +36 -0
- data/solr/solr/conf/protwords.txt +21 -0
- data/solr/solr/conf/schema.xml +50 -0
- data/solr/solr/conf/solrconfig.xml +696 -0
- data/solr/solr/conf/stopwords.txt +57 -0
- data/solr/solr/conf/synonyms.txt +31 -0
- data/solr/start.jar +0 -0
- data/solr/webapps/solr.war +0 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/build_search_spec.rb +918 -0
- data/spec/api/indexer_spec.rb +311 -0
- data/spec/api/query_spec.rb +153 -0
- data/spec/api/search_retrieval_spec.rb +325 -0
- data/spec/api/session_spec.rb +157 -0
- data/spec/api/spec_helper.rb +1 -0
- data/spec/api/sunspot_spec.rb +18 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/integration/faceting_spec.rb +169 -0
- data/spec/integration/keyword_search_spec.rb +83 -0
- data/spec/integration/scoped_search_spec.rb +188 -0
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/integration/test_pagination.rb +32 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +19 -0
- data/spec/mocks/connection.rb +84 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_record.rb +41 -0
- data/spec/mocks/photo.rb +8 -0
- data/spec/mocks/post.rb +70 -0
- data/spec/mocks/user.rb +8 -0
- data/spec/spec_helper.rb +47 -0
- data/tasks/gemspec.rake +25 -0
- data/tasks/rcov.rake +28 -0
- data/tasks/rdoc.rake +21 -0
- data/tasks/schema.rake +19 -0
- data/tasks/spec.rake +24 -0
- data/tasks/todo.rake +4 -0
- data/templates/schema.xml.haml +24 -0
- metadata +245 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# The Scope class encapsulates a set of restrictions that scope search
|
5
|
+
# results (as well as query facets rows). This class's API is exposed by
|
6
|
+
# Query::Query and Query::QueryFacetRow.
|
7
|
+
#
|
8
|
+
class Scope
|
9
|
+
#
|
10
|
+
# Add a restriction to the query.
|
11
|
+
#
|
12
|
+
# ==== Parameters
|
13
|
+
#
|
14
|
+
# field_name<Symbol>:: Name of the field to which the restriction applies
|
15
|
+
# restriction_type<Class,Symbol>::
|
16
|
+
# Subclass of Sunspot::Query::Restriction::Base, or snake_cased name as symbol
|
17
|
+
# (e.g., +:equal_to+)
|
18
|
+
# value<Object>::
|
19
|
+
# Value against which the restriction applies (e.g. less_than(2) has a
|
20
|
+
# value of 2)
|
21
|
+
# negated::
|
22
|
+
# Whether this restriction should be negated (use add_negated_restriction)
|
23
|
+
#
|
24
|
+
def add_restriction(field_name, restriction_type, value, negated = false)
|
25
|
+
if restriction_type.is_a?(Symbol)
|
26
|
+
restriction_type = Restriction[restriction_type]
|
27
|
+
end
|
28
|
+
add_component(
|
29
|
+
restriction = restriction_type.new(
|
30
|
+
build_field(field_name), value, negated
|
31
|
+
)
|
32
|
+
)
|
33
|
+
restriction
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Add a negated restriction to the query. The restriction will be taken as
|
38
|
+
# the opposite of its usual meaning (e.g., an :equal_to restriction will
|
39
|
+
# be "not equal to".
|
40
|
+
#
|
41
|
+
# ==== Parameters
|
42
|
+
#
|
43
|
+
# field_name<Symbol>:: Name of the field to which the restriction applies
|
44
|
+
# restriction_type<Class>::
|
45
|
+
# Subclass of Sunspot::Query::Restriction::Base to instantiate
|
46
|
+
# value<Object>::
|
47
|
+
# Value against which the restriction applies (e.g. less_than(2) has a
|
48
|
+
# value of 2)
|
49
|
+
#
|
50
|
+
def add_negated_restriction(field_name, restriction_type, value)
|
51
|
+
add_restriction(field_name, restriction_type, value, true)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Add a disjunction to the scope. The disjunction can then take a set of
|
56
|
+
# restrictions, which are combined with OR semantics.
|
57
|
+
#
|
58
|
+
# ==== Returns
|
59
|
+
#
|
60
|
+
# Connective::Disjunction:: New disjunction
|
61
|
+
#
|
62
|
+
def add_disjunction
|
63
|
+
add_component(disjunction = Connective::Disjunction.new(setup))
|
64
|
+
disjunction
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Add a conjunction to the scope. In most cases, this will simply return
|
69
|
+
# the Scope object itself, since scopes by default combine their
|
70
|
+
# restrictions with OR semantics. The Connective::Disjunction class
|
71
|
+
# overrides this method to return a Connective::Conjunction.
|
72
|
+
#
|
73
|
+
# ==== Returns
|
74
|
+
#
|
75
|
+
# Scope:: Self or another scope with conjunctive semantics.
|
76
|
+
#
|
77
|
+
def add_conjunction
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Exclude a particular instance from the search results
|
83
|
+
#
|
84
|
+
# ==== Parameters
|
85
|
+
#
|
86
|
+
# instance<Object>:: instance to exclude from results
|
87
|
+
#
|
88
|
+
def exclude_instance(instance)
|
89
|
+
add_component(Restriction::SameAs.new(instance, true))
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Generate a DynamicQuery instance for the given base name.
|
94
|
+
# This gives you access to a subset of the Query API but the operations
|
95
|
+
# apply to dynamic fields inside the dynamic field definition specified
|
96
|
+
# by +base_name+.
|
97
|
+
#
|
98
|
+
# ==== Parameters
|
99
|
+
#
|
100
|
+
# base_name<Symbol>::
|
101
|
+
# Base name of the dynamic field definition to use in the dynamic query
|
102
|
+
# operations
|
103
|
+
#
|
104
|
+
# ==== Returns
|
105
|
+
#
|
106
|
+
# DynamicQuery::
|
107
|
+
# Instance providing dynamic query functionality for the given field
|
108
|
+
# definitions.
|
109
|
+
#
|
110
|
+
def dynamic_query(base_name)
|
111
|
+
DynamicQuery.new(setup.dynamic_field_factory(base_name), self)
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Determine which restriction type to add based on the type of the value.
|
116
|
+
# Used to interpret query conditions passed as a hash, as well as the
|
117
|
+
# short-form DSL::Scope#with method.
|
118
|
+
#
|
119
|
+
# ==== Parameters
|
120
|
+
#
|
121
|
+
# field_name<Symbol>:: Name of the field on which to apply the restriction
|
122
|
+
# value<Object,Array,Range>:: Value to which to apply to the restriction
|
123
|
+
#--
|
124
|
+
# negated<Boolean>:: Whether to negate the restriction.
|
125
|
+
#
|
126
|
+
def add_shorthand_restriction(field_name, value, negated = false) #:nodoc:
|
127
|
+
restriction_type =
|
128
|
+
case value
|
129
|
+
when Range
|
130
|
+
Restriction::Between
|
131
|
+
when Array
|
132
|
+
Restriction::AnyOf
|
133
|
+
else
|
134
|
+
Restriction::EqualTo
|
135
|
+
end
|
136
|
+
add_restriction(field_name, restriction_type, value, negated)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Add a negated shorthand restriction. See #add_shorthand_restriction
|
141
|
+
#
|
142
|
+
def add_negated_shorthand_restriction(field_name, value)
|
143
|
+
add_shorthand_restriction(field_name, value, true)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
#
|
149
|
+
# Build a field with the given field name. Subclasses may override this
|
150
|
+
# method.
|
151
|
+
#
|
152
|
+
def build_field(field_name)
|
153
|
+
setup.field(field_name)
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
# Return a setup object which can return a field object given a name.
|
158
|
+
# Subclasses may override this method.
|
159
|
+
#
|
160
|
+
def setup
|
161
|
+
@setup
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# The Sort class is a query component representing a sort by a given field.
|
5
|
+
#
|
6
|
+
class Sort #:nodoc:
|
7
|
+
DIRECTIONS = {
|
8
|
+
:asc => 'asc',
|
9
|
+
:ascending => 'asc',
|
10
|
+
:desc => 'desc',
|
11
|
+
:descending => 'desc'
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(field, direction = nil)
|
15
|
+
if field.multiple?
|
16
|
+
raise(ArgumentError, "#{field.name} cannot be used for ordering because it is a multiple-value field")
|
17
|
+
end
|
18
|
+
@field, @direction = field, (direction || :asc).to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_param
|
22
|
+
"#{@field.indexed_name.to_sym} #{direction_for_solr}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def direction_for_solr
|
28
|
+
DIRECTIONS[@direction] ||
|
29
|
+
raise(
|
30
|
+
ArgumentError,
|
31
|
+
"Unknown sort direction #{@direction}. Acceptable input is: #{DIRECTIONS.keys.map { |input| input.inspect } * ', '}"
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# The SortComposite class encapsulates an ordered collection of Sort
|
5
|
+
# objects. It's necessary to keep this as a separate class as Solr takes
|
6
|
+
# the sort as a single parameter, so adding sorts as regular components
|
7
|
+
# would not merge correctly in the #to_params method.
|
8
|
+
#
|
9
|
+
class SortComposite #:nodoc:
|
10
|
+
def initialize
|
11
|
+
@sorts = []
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Add a sort to the composite
|
16
|
+
#
|
17
|
+
def <<(sort)
|
18
|
+
@sorts << sort
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Combine the sorts into a single param by joining them
|
23
|
+
#
|
24
|
+
def to_params
|
25
|
+
unless @sorts.empty?
|
26
|
+
{ :sort => @sorts.map { |sort| sort.to_param } * ', ' }
|
27
|
+
else
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Sunspot
|
2
|
+
#
|
3
|
+
# QueryFacet instances encapsulate a set of query facet results. Each facet
|
4
|
+
# corresponds to a group of rows defined inside a DSL::FieldQuery#facet block.
|
5
|
+
#
|
6
|
+
class QueryFacet
|
7
|
+
def initialize(outgoing_query_facet, row_data) #:nodoc:
|
8
|
+
@outgoing_query_facet, @row_data = outgoing_query_facet, row_data
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Get the rows associated with this query facet. Returned rows are always
|
13
|
+
# ordered by count.
|
14
|
+
#
|
15
|
+
# ==== Returns
|
16
|
+
#
|
17
|
+
# Array:: Collection of QueryFacetRow objects, ordered by count
|
18
|
+
#
|
19
|
+
def rows
|
20
|
+
@rows ||=
|
21
|
+
begin
|
22
|
+
rows = []
|
23
|
+
for row in @outgoing_query_facet.rows
|
24
|
+
row_query = row.to_boolean_phrase
|
25
|
+
if @row_data.has_key?(row_query)
|
26
|
+
rows << QueryFacetRow.new(row.label, @row_data[row_query])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
rows.sort! { |x, y| y.count <=> x.count }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sunspot
|
2
|
+
#
|
3
|
+
# Objects of this class encapsulate a single query facet row returned for a
|
4
|
+
# query facet.
|
5
|
+
#
|
6
|
+
class QueryFacetRow
|
7
|
+
#
|
8
|
+
# This is the "label" passed into the query facet row when it is defined in
|
9
|
+
# the search.
|
10
|
+
#
|
11
|
+
attr_reader :value
|
12
|
+
#
|
13
|
+
# Number of documents in the result set that match this facet's scope.
|
14
|
+
#
|
15
|
+
attr_reader :count
|
16
|
+
|
17
|
+
def initialize(value, count) #:nodoc:
|
18
|
+
@value, @count = value, count
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
using_rubygems = false
|
2
|
+
begin
|
3
|
+
require 'haml'
|
4
|
+
rescue LoadError => e
|
5
|
+
if using_rubygems
|
6
|
+
raise(e)
|
7
|
+
else
|
8
|
+
using_rubygems = true
|
9
|
+
require 'rubygems'
|
10
|
+
retry
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Sunspot
|
15
|
+
#
|
16
|
+
# Object that encapsulates schema information for building a Solr schema.xml
|
17
|
+
# file. This class is used by the schema:compile task as well as the
|
18
|
+
# sunspot-configure-solr executable.
|
19
|
+
#
|
20
|
+
class Schema #:nodoc:all
|
21
|
+
FieldType = Struct.new(:name, :class_name, :suffix)
|
22
|
+
FieldVariant = Struct.new(:attribute, :suffix)
|
23
|
+
|
24
|
+
DEFAULT_TOKENIZER = 'solr.StandardTokenizerFactory'
|
25
|
+
DEFAULT_FILTERS = %w(solr.StandardFilterFactory solr.LowerCaseFilterFactory)
|
26
|
+
|
27
|
+
FIELD_TYPES = [
|
28
|
+
FieldType.new('boolean', 'Bool', 'b'),
|
29
|
+
FieldType.new('sfloat', 'SortableFloat', 'f'),
|
30
|
+
FieldType.new('date', 'Date', 'd'),
|
31
|
+
FieldType.new('sint', 'SortableInt', 'i'),
|
32
|
+
FieldType.new('string', 'Str', 's')
|
33
|
+
]
|
34
|
+
|
35
|
+
FIELD_VARIANTS = [
|
36
|
+
FieldVariant.new('multiValued', 'm'),
|
37
|
+
FieldVariant.new('stored', 's')
|
38
|
+
]
|
39
|
+
|
40
|
+
attr_reader :tokenizer, :filters
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@tokenizer = DEFAULT_TOKENIZER
|
44
|
+
@filters = DEFAULT_FILTERS.dup
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Attribute field types defined in the schema
|
49
|
+
#
|
50
|
+
def types
|
51
|
+
FIELD_TYPES
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# DynamicField instances representing all the available types and variants
|
56
|
+
#
|
57
|
+
def dynamic_fields
|
58
|
+
fields = []
|
59
|
+
for field_variants in variant_combinations
|
60
|
+
for type in FIELD_TYPES
|
61
|
+
fields << DynamicField.new(type, field_variants)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
fields
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Which tokenizer to use for text fields
|
69
|
+
#
|
70
|
+
def tokenizer=(tokenizer)
|
71
|
+
@tokenizer =
|
72
|
+
if tokenizer =~ /\./
|
73
|
+
tokenizer
|
74
|
+
else
|
75
|
+
"solr.#{tokenizer}TokenizerFactory"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Add a filter for text field tokenization
|
81
|
+
#
|
82
|
+
def add_filter(filter)
|
83
|
+
@filters <<
|
84
|
+
if filter =~ /\./
|
85
|
+
filter
|
86
|
+
else
|
87
|
+
"solr.#{filter}FilterFactory"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Return an XML representation of this schema using the Haml template
|
93
|
+
#
|
94
|
+
def to_xml
|
95
|
+
template = File.read(
|
96
|
+
File.join(
|
97
|
+
File.dirname(__FILE__),
|
98
|
+
'..',
|
99
|
+
'..',
|
100
|
+
'templates',
|
101
|
+
'schema.xml.haml'
|
102
|
+
)
|
103
|
+
)
|
104
|
+
engine = Haml::Engine.new(template)
|
105
|
+
engine.render(Object.new, :schema => self)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
#
|
111
|
+
# All of the possible combinations of variants
|
112
|
+
#
|
113
|
+
def variant_combinations
|
114
|
+
combinations = []
|
115
|
+
0.upto(2 ** FIELD_VARIANTS.length - 1) do |b|
|
116
|
+
combinations << combination = []
|
117
|
+
FIELD_VARIANTS.each_with_index do |variant, i|
|
118
|
+
combination << variant if b & 1<<i > 0
|
119
|
+
end
|
120
|
+
end
|
121
|
+
combinations
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Represents a dynamic field (in the Solr schema sense, not the Sunspot
|
126
|
+
# sense).
|
127
|
+
#
|
128
|
+
class DynamicField
|
129
|
+
def initialize(type, field_variants)
|
130
|
+
@type, @field_variants = type, field_variants
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Name of the field in the schema
|
135
|
+
#
|
136
|
+
def name
|
137
|
+
variant_suffixes = @field_variants.map { |variant| variant.suffix }.join
|
138
|
+
"*_#{@type.suffix}#{variant_suffixes}"
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Name of the type as defined in the schema
|
143
|
+
#
|
144
|
+
def type
|
145
|
+
@type.name
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Implement magic methods to ask if a field is of a particular variant.
|
150
|
+
# Returns "true" if the field is of that variant and "false" otherwise.
|
151
|
+
#
|
152
|
+
def method_missing(name, *args, &block)
|
153
|
+
if name.to_s =~ /\?$/ && args.empty?
|
154
|
+
if @field_variants.any? { |variant| "#{variant.attribute}?" == name.to_s }
|
155
|
+
'true'
|
156
|
+
else
|
157
|
+
'false'
|
158
|
+
end
|
159
|
+
else
|
160
|
+
super(name.to_sym, *args, &block)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|