lucid_works 0.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/.autotest +1 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +11 -0
- data/Rakefile +7 -0
- data/lib/lucid_works.rb +26 -0
- data/lib/lucid_works/associations.rb +118 -0
- data/lib/lucid_works/base.rb +274 -0
- data/lib/lucid_works/collection.rb +14 -0
- data/lib/lucid_works/collection/index.rb +9 -0
- data/lib/lucid_works/collection/info.rb +9 -0
- data/lib/lucid_works/collection/settings.rb +9 -0
- data/lib/lucid_works/datasource.rb +40 -0
- data/lib/lucid_works/datasource/history.rb +17 -0
- data/lib/lucid_works/datasource/index.rb +9 -0
- data/lib/lucid_works/datasource/schedule.rb +9 -0
- data/lib/lucid_works/datasource/status.rb +9 -0
- data/lib/lucid_works/exceptions.rb +6 -0
- data/lib/lucid_works/patch_restclient.rb +29 -0
- data/lib/lucid_works/server.rb +23 -0
- data/lib/lucid_works/version.rb +3 -0
- data/lucid_works.gemspec +26 -0
- data/spec/lib/lucid_works/associations_spec.rb +130 -0
- data/spec/lib/lucid_works/base_spec.rb +399 -0
- data/spec/lib/lucid_works/collection_spec.rb +231 -0
- data/spec/lib/lucid_works/datasource_spec.rb +280 -0
- data/spec/lib/lucid_works/server_spec.rb +39 -0
- data/spec/spec_helper.rb +47 -0
- metadata +133 -0
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LucidWorks::Collection do
|
4
|
+
before :all do
|
5
|
+
@server = connect_to_live_server
|
6
|
+
@server.reset_collections!
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "CRUD" do
|
10
|
+
before do
|
11
|
+
@collection_name = "a_new_collection"
|
12
|
+
@instance_dir = "my_instance_dir"
|
13
|
+
end
|
14
|
+
|
15
|
+
def collection_count
|
16
|
+
LucidWorks::Collection.all(:parent => @server).size
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ".create" do
|
20
|
+
context "with ideal circumstances" do
|
21
|
+
it "should create a new collection with the appropriate name" do
|
22
|
+
lambda {
|
23
|
+
c = LucidWorks::Collection.create(:name => @collection_name,
|
24
|
+
:instance_dir => @instance_dir,
|
25
|
+
:parent => @server)
|
26
|
+
c.should be_kind_of(LucidWorks::Collection)
|
27
|
+
c.name.should == @collection_name
|
28
|
+
c.instance_dir.should == @instance_dir
|
29
|
+
c.should be_persisted
|
30
|
+
}.should change(self, :collection_count).by(1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "without a name" do
|
35
|
+
it "should not create" do
|
36
|
+
c = LucidWorks::Collection.create(:name => '', :parent => @server)
|
37
|
+
c.should_not be_persisted
|
38
|
+
c.errors.should_not be_empty
|
39
|
+
c.errors[:name].should_not be_blank
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "with a conflicting name" do
|
44
|
+
before do
|
45
|
+
@conflicting_name = "already_extant_collection"
|
46
|
+
LucidWorks::Collection.create(:name => @conflicting_name, :parent => @server)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should detect a conflict error" do
|
50
|
+
c = LucidWorks::Collection.create(:name => @conflicting_name, :parent => @server)
|
51
|
+
c.should_not be_persisted
|
52
|
+
c.errors[:name].should include "collection already exists"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ".find" do
|
58
|
+
context "for a nonexistant collection" do
|
59
|
+
it "should raise an exception" do
|
60
|
+
lambda {
|
61
|
+
LucidWorks::Collection.find('nonexistant_collection', :parent => @server)
|
62
|
+
}.should raise_error(RestClient::ResourceNotFound)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "for an extant collection" do
|
67
|
+
it "should return the collection" do
|
68
|
+
c = LucidWorks::Collection.find(@collection_name, :parent => @server)
|
69
|
+
c.should be_kind_of(LucidWorks::Collection)
|
70
|
+
c.name.should == @collection_name
|
71
|
+
c.instance_dir.should == @instance_dir
|
72
|
+
c.should be_persisted
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#destroy" do
|
78
|
+
it "should delete the collection" do
|
79
|
+
c = @server.collection(@collection_name)
|
80
|
+
lambda {
|
81
|
+
c.destroy
|
82
|
+
}.should change(self, :collection_count).by(-1)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#info" do
|
88
|
+
before :all do
|
89
|
+
@collection = LucidWorks::Collection.first(:parent => @server)
|
90
|
+
@info = @collection.info
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should return a valid LucidWorks::Collection::Info" do
|
94
|
+
@info.should be_a(LucidWorks::Collection::Info)
|
95
|
+
@info.should be_valid
|
96
|
+
end
|
97
|
+
|
98
|
+
%w{ free_disk_space index_last_modified }.each do |attr|
|
99
|
+
it "should have a value for #{attr}" do
|
100
|
+
@info.send(attr.to_sym).should_not be_nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "#settings" do
|
106
|
+
before :all do
|
107
|
+
@collection = LucidWorks::Collection.first(:parent => @server)
|
108
|
+
@settings = @collection.settings
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should return a valid LucidWorks::Collection::Settings" do
|
112
|
+
@settings.should be_a(LucidWorks::Collection::Settings)
|
113
|
+
@settings.should be_valid
|
114
|
+
end
|
115
|
+
|
116
|
+
%w{ spellcheck display_facets }.each do |attr|
|
117
|
+
it "should have a value for #{attr}" do
|
118
|
+
@settings.send(attr.to_sym).should_not be_nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "for a collection with 2 datasources" do
|
124
|
+
before :all do
|
125
|
+
@collection = LucidWorks::Collection.create(:name => "collection-for-testing-datasources", :parent => @server)
|
126
|
+
@collection.should be_valid
|
127
|
+
@ds1 = LucidWorks::Datasource.create(
|
128
|
+
:collection => @collection,
|
129
|
+
:crawler => "lucid.aperture",
|
130
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
131
|
+
:name => "US zoneinfo",
|
132
|
+
:path => "/usr/share/zoneinfo/US",
|
133
|
+
:crawl_depth => 1
|
134
|
+
)
|
135
|
+
@ds1.should be_valid
|
136
|
+
@ds2 = LucidWorks::Datasource.create(
|
137
|
+
:collection => @collection,
|
138
|
+
:crawler => "lucid.aperture",
|
139
|
+
:type => LucidWorks::Datasource::TYPES[:WebDataSource][:type],
|
140
|
+
:name => "nothing_com",
|
141
|
+
:url => "http://nothing.com/",
|
142
|
+
:crawl_depth => 1
|
143
|
+
)
|
144
|
+
@ds2.should be_valid
|
145
|
+
# Schedule a crawl to create some history
|
146
|
+
@schedule = @ds1.build_schedule
|
147
|
+
@schedule.period = '0'
|
148
|
+
@schedule.start_time = '0'
|
149
|
+
@schedule.active = true
|
150
|
+
@schedule.type = 'index'
|
151
|
+
@schedule.save
|
152
|
+
begin
|
153
|
+
Timeout::timeout(60) do
|
154
|
+
begin
|
155
|
+
sleep 1
|
156
|
+
status = @ds1.status!
|
157
|
+
end while status.crawlState != 'FINISHED'
|
158
|
+
end
|
159
|
+
rescue Timeout::Error
|
160
|
+
raise "Timed out waiting for collection #{@collection.name} datasource #{@ds1.name} crawl to complete"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#datasource" do
|
165
|
+
it "should retrieve a single data source" do
|
166
|
+
ds = @collection.datasource(@ds1.id)
|
167
|
+
ds.should be_a(LucidWorks::Datasource)
|
168
|
+
ds.name.should == @ds1.name
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe '#datasources' do
|
173
|
+
|
174
|
+
it "should retrieve an array of data sources" do
|
175
|
+
datasources = @collection.datasources
|
176
|
+
datasources.should be_a(Array)
|
177
|
+
datasources.size.should == 2
|
178
|
+
datasources.each { |ds| ds.should be_a(LucidWorks::Datasource) }
|
179
|
+
datasources.map(&:name).should =~ [@ds1.name, @ds2.name]
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should put a reference back to the collection in the datasources" do
|
183
|
+
datasources = @collection.datasources
|
184
|
+
datasources.each do |ds|
|
185
|
+
ds.collection.should == @collection
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# context "with a bogus :include option" do
|
190
|
+
# it "should raise an error" do
|
191
|
+
# lambda {
|
192
|
+
# @collection.datasources(:include => :sprinkles)
|
193
|
+
# }.should raise_error(RuntimeError)
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
|
197
|
+
context "with option include :status" do
|
198
|
+
it "should attempt to retrieve all datasources and their status" do
|
199
|
+
datasources = @collection.datasources(:include => :status)
|
200
|
+
datasource = datasources.detect { |ds| ds.id == @ds1.id }
|
201
|
+
status = datasource.status
|
202
|
+
status.should be_a(LucidWorks::Datasource::Status)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
context "with option include :history and :status" do
|
207
|
+
it "should attempt to retrieve all datasources and their statuses and history" do
|
208
|
+
datasources = @collection.datasources(:include => [:histories, :status])
|
209
|
+
datasource = datasources.detect { |ds| ds.id == @ds1.id }
|
210
|
+
datasource.histories.first.should be_a(LucidWorks::Datasource::History)
|
211
|
+
datasource.histories.first.crawlState.should == "FINISHED"
|
212
|
+
status = datasource.status
|
213
|
+
status.should be_a(LucidWorks::Datasource::Status)
|
214
|
+
status.crawlState.should == 'FINISHED'
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe "#empty!" do
|
221
|
+
before do
|
222
|
+
@collection = @server.collections.first
|
223
|
+
end
|
224
|
+
|
225
|
+
it "should DELETE /api/collections/<collection_id>/index" do
|
226
|
+
RestClient.should_receive(:delete).with("#{@collection.uri}/index", :params => {:key => 'iaccepttherisk'})
|
227
|
+
|
228
|
+
@collection.empty!
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LucidWorks::Datasource do
|
4
|
+
before :all do
|
5
|
+
@server = connect_to_live_server
|
6
|
+
@server.reset_collections!
|
7
|
+
@collection = @server.collections.first
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "CRUD" do
|
11
|
+
|
12
|
+
def datasource_count
|
13
|
+
@collection.datasources!.size
|
14
|
+
end
|
15
|
+
|
16
|
+
describe ".create" do
|
17
|
+
|
18
|
+
context "with no parameters" do
|
19
|
+
before do
|
20
|
+
@parameters = {
|
21
|
+
:parent => @collection
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not create a datasource, and add a base error" do
|
26
|
+
d = nil
|
27
|
+
lambda {
|
28
|
+
d = LucidWorks::Datasource.create(@parameters)
|
29
|
+
}.should_not change(self, :datasource_count)
|
30
|
+
d.errors[:base].should == ["No input content found"]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "without a crawler specified" do
|
35
|
+
before do
|
36
|
+
@parameters = {
|
37
|
+
:collection => @collection,
|
38
|
+
:type => LucidWorks::Datasource::TYPES[:WebDataSource][:type],
|
39
|
+
:name => "foo"
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not create a datasource, and add an invalid error to crawler" do
|
44
|
+
d = nil
|
45
|
+
lambda {
|
46
|
+
d = LucidWorks::Datasource.create(@parameters)
|
47
|
+
}.should_not change(self, :datasource_count)
|
48
|
+
d.errors[:crawler].should_not be_empty
|
49
|
+
d.errors[:base].should be_blank
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "without a datasource type" do
|
54
|
+
before do
|
55
|
+
@parameters = {
|
56
|
+
:parent => @collection,
|
57
|
+
:name => "foo",
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not create a datasource, and add an invalid error to type" do
|
62
|
+
d = nil
|
63
|
+
lambda {
|
64
|
+
d = LucidWorks::Datasource.create(@parameters)
|
65
|
+
}.should_not change(self, :datasource_count)
|
66
|
+
d.errors[:type].should_not be_empty
|
67
|
+
d.errors[:base].should be_blank
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with invalid parameters" do
|
72
|
+
before do
|
73
|
+
@parameters = {
|
74
|
+
:collection => @collection,
|
75
|
+
:name => "foo",
|
76
|
+
:crawler => "lucid.aperture",
|
77
|
+
:type => LucidWorks::Datasource::TYPES[:WebDataSource][:type],
|
78
|
+
:url => "invalid url",
|
79
|
+
:crawl_depth => "invalid crawl depth"
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should not create a datasource, and add errors for the invalid parameters" do
|
84
|
+
d = nil
|
85
|
+
lambda {
|
86
|
+
d = LucidWorks::Datasource.create(@parameters)
|
87
|
+
}.should_not change(self, :datasource_count)
|
88
|
+
d.errors[:url].should_not be_empty
|
89
|
+
d.errors[:crawl_depth].should_not be_empty
|
90
|
+
d.errors[:base].should be_blank
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "with valid parameters" do
|
95
|
+
before do
|
96
|
+
@parameters = {
|
97
|
+
:collection => @collection,
|
98
|
+
:crawler => "lucid.aperture",
|
99
|
+
:type => LucidWorks::Datasource::TYPES[:WebDataSource][:type],
|
100
|
+
:name => "Lucid Imagination Website",
|
101
|
+
:url => "http://www.lucidimagination.com/",
|
102
|
+
:crawl_depth => 2,
|
103
|
+
:exclude_paths => [
|
104
|
+
"http://www\\.lucidimagination\\.com/blog/tag/.*",
|
105
|
+
"http://www\\.lucidimagination\\.com/search\\?.*"
|
106
|
+
],
|
107
|
+
:include_paths => [
|
108
|
+
"http://www\\.lucidimagination\\.com/.*"
|
109
|
+
]
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should create a datasource" do
|
114
|
+
lambda {
|
115
|
+
d = LucidWorks::Datasource.create(@parameters)
|
116
|
+
d.should be_kind_of(LucidWorks::Datasource)
|
117
|
+
d.should be_persisted
|
118
|
+
}.should change(self, :datasource_count).by(1)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe ".find" do
|
124
|
+
context "for an extant datasource" do
|
125
|
+
before do
|
126
|
+
@datasource = LucidWorks::Datasource.create(
|
127
|
+
:collection => @collection,
|
128
|
+
:crawler => "lucid.aperture",
|
129
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
130
|
+
:name => "slash_temp",
|
131
|
+
:path => "/tmp",
|
132
|
+
:crawl_depth => 2
|
133
|
+
)
|
134
|
+
@datasource.should be_persisted
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should retrieve the datasource" do
|
138
|
+
d = LucidWorks::Datasource.find(@datasource.id, :collection => @collection)
|
139
|
+
d.should be_persisted
|
140
|
+
d.name.should == @datasource.name
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#save" do
|
146
|
+
context "for an extant datasource" do
|
147
|
+
before do
|
148
|
+
@datasource = LucidWorks::Datasource.create(
|
149
|
+
:collection => @collection,
|
150
|
+
:crawler => "lucid.aperture",
|
151
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
152
|
+
:name => "datasource we are going to update",
|
153
|
+
:path => "/fake_ds_to_be_updated",
|
154
|
+
:crawl_depth => 2
|
155
|
+
)
|
156
|
+
@datasource.should be_persisted
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should update the datasource" do
|
160
|
+
@datasource.name = "updated datasource"
|
161
|
+
@datasource.save
|
162
|
+
LucidWorks::Datasource.find(@datasource.id, :collection => @collection).name.should == "updated datasource"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "#destroy" do
|
168
|
+
context "for an extant datasource" do
|
169
|
+
before do
|
170
|
+
@datasource = LucidWorks::Datasource.create(
|
171
|
+
:collection => @collection,
|
172
|
+
:crawler => "lucid.aperture",
|
173
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
174
|
+
:name => "datasource we are going to delete",
|
175
|
+
:path => "/fake_ds_to_be_deleted",
|
176
|
+
:crawl_depth => 2
|
177
|
+
)
|
178
|
+
@datasource.should be_persisted
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should delete the datasource" do
|
182
|
+
lambda {
|
183
|
+
@datasource.destroy
|
184
|
+
}.should change(self, :datasource_count).by(-1)
|
185
|
+
lambda {
|
186
|
+
LucidWorks::Datasource.find(@datasource.id, :collection => @collection)
|
187
|
+
}.should raise_error(RestClient::ResourceNotFound)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "#status" do
|
194
|
+
before :all do
|
195
|
+
@datasource = LucidWorks::Datasource.create(
|
196
|
+
:collection => @collection,
|
197
|
+
:crawler => "lucid.aperture",
|
198
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
199
|
+
:name => "datasource we are going to get a status of",
|
200
|
+
:path => "/fake_ds_to_get_status_of",
|
201
|
+
:crawl_depth => 2
|
202
|
+
)
|
203
|
+
@datasource.should be_persisted
|
204
|
+
@status = @datasource.status
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should return a valid LucidWorks::Collection::Datasource::Status" do
|
208
|
+
@status.should be_a(LucidWorks::Datasource::Status)
|
209
|
+
@status.should be_valid
|
210
|
+
end
|
211
|
+
|
212
|
+
%w{ crawlState numUnchanged }.each do |attr|
|
213
|
+
it "should have a value for #{attr}" do
|
214
|
+
@status.send(attr.to_sym).should_not be_nil
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#schedule" do
|
220
|
+
it "should retrieve the datasource's schedule" do
|
221
|
+
@datasource = LucidWorks::Datasource.create(
|
222
|
+
:collection => @collection,
|
223
|
+
:crawler => "lucid.aperture",
|
224
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
225
|
+
:name => "datasource we are going to get a schedule of",
|
226
|
+
:path => "/fake_ds_to_get_schedule_of",
|
227
|
+
:crawl_depth => 2
|
228
|
+
)
|
229
|
+
@datasource.should be_persisted
|
230
|
+
|
231
|
+
schedule = @datasource.schedule
|
232
|
+
schedule.should be_a(LucidWorks::Datasource::Schedule)
|
233
|
+
schedule.should be_persisted
|
234
|
+
schedule.datasource.should == @datasource
|
235
|
+
schedule.type.should == "index"
|
236
|
+
schedule.period.should == 0
|
237
|
+
schedule.active.should == false
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe "#build_index" do
|
242
|
+
it "should return a new LucidWorks::Datasource::Index for this datasource" do
|
243
|
+
@datasource = LucidWorks::Datasource.create(
|
244
|
+
:collection => @collection,
|
245
|
+
:crawler => "lucid.aperture",
|
246
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
247
|
+
:name => "datasource we are going to get a index of",
|
248
|
+
:path => "/fake_ds_to_get_index_of",
|
249
|
+
:crawl_depth => 2
|
250
|
+
)
|
251
|
+
@datasource.should be_valid
|
252
|
+
|
253
|
+
index = @datasource.build_index
|
254
|
+
index.should be_a(LucidWorks::Datasource::Index)
|
255
|
+
index.should be_persisted # special case - singletons are always considered persisted
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
describe "#empty!" do
|
260
|
+
before do
|
261
|
+
@collection = @server.collections.first
|
262
|
+
@datasource = LucidWorks::Datasource.create(
|
263
|
+
:collection => @collection,
|
264
|
+
:crawler => "lucid.aperture",
|
265
|
+
:type => LucidWorks::Datasource::TYPES[:FileSystemDataSource][:type],
|
266
|
+
:name => "datasource we are going to empty",
|
267
|
+
:path => "/fake_ds_to_empty",
|
268
|
+
:crawl_depth => 2
|
269
|
+
)
|
270
|
+
@datasource.should be_valid
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should DELETE /api/collections/<collection_id>/index" do
|
274
|
+
RestClient.should_receive(:delete).with("#{@datasource.uri}/index", {})
|
275
|
+
|
276
|
+
@datasource.empty!
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|