dca 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +2 -0
- data/Gemfile +48 -0
- data/Gemfile.lock +126 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/bin/dca +5 -0
- data/dca.gemspec +160 -0
- data/lib/dca.rb +64 -0
- data/lib/dca/cli.rb +32 -0
- data/lib/dca/commands/area.rb +133 -0
- data/lib/dca/commands/templates/area/analyzer.rb.erb +34 -0
- data/lib/dca/commands/templates/area/area.rb.erb +2 -0
- data/lib/dca/commands/templates/area/models.rb.erb +2 -0
- data/lib/dca/commands/templates/area/page.rb.erb +17 -0
- data/lib/dca/commands/templates/area/position.rb.erb +8 -0
- data/lib/dca/commands/templates/config.yml.erb +38 -0
- data/lib/dca/commands/templates/spec/analyzer_spec.rb.erb +15 -0
- data/lib/dca/commands/templates/spec/spec_helper.rb.erb +2 -0
- data/lib/dca/config.rb +20 -0
- data/lib/dca/helpers.rb +2 -0
- data/lib/dca/helpers/logger.rb +50 -0
- data/lib/dca/jobs.rb +3 -0
- data/lib/dca/jobs/analyzer_job.rb +119 -0
- data/lib/dca/jobs/job.rb +62 -0
- data/lib/dca/models.rb +5 -0
- data/lib/dca/models/base_model.rb +73 -0
- data/lib/dca/models/binder.rb +68 -0
- data/lib/dca/models/binder_helper.rb +48 -0
- data/lib/dca/models/nokogiri_binder.rb +43 -0
- data/lib/dca/models/position.rb +15 -0
- data/lib/dca/net.rb +1 -0
- data/lib/dca/net/browser_helper.rb +20 -0
- data/lib/dca/notifier.rb +2 -0
- data/lib/dca/notifier/notifier.rb +11 -0
- data/lib/dca/notifier/redis/models/analyze_notify.rb +12 -0
- data/lib/dca/notifier/redis/models/failure_notify.rb +8 -0
- data/lib/dca/notifier/redis/models/fetch_notify.rb +15 -0
- data/lib/dca/notifier/redis/models/session.rb +52 -0
- data/lib/dca/notifier/redis/notifier.rb +25 -0
- data/lib/dca/notifier/redis_notifier.rb +9 -0
- data/lib/dca/storage.rb +3 -0
- data/lib/dca/storage/elasticsearch_storage.rb +80 -0
- data/lib/dca/storage/mongo_storage.rb +51 -0
- data/lib/dca/storage/storage.rb +55 -0
- data/spec/analyzer_spec.rb +64 -0
- data/spec/area_task_spec.rb +45 -0
- data/spec/base_model_spec.rb +34 -0
- data/spec/binder_spec.rb +69 -0
- data/spec/config.yml +18 -0
- data/spec/elasticsearch_storage_spec.rb +28 -0
- data/spec/fixtures/page.html +12 -0
- data/spec/fixtures/positions.yml +13 -0
- data/spec/fixtures/positions_with_error.yml +14 -0
- data/spec/fixtures/states.yml +3 -0
- data/spec/job_spec.rb +31 -0
- data/spec/mock/analyzer_job.rb +30 -0
- data/spec/mock/file_storage.rb +28 -0
- data/spec/mock/notify_object.rb +13 -0
- data/spec/mock/page.rb +13 -0
- data/spec/mock/position.rb +40 -0
- data/spec/mock/web_notifier.rb +30 -0
- data/spec/mongo_storage_spec.rb +20 -0
- data/spec/redis_notifier_spec.rb +98 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/storage_examples.rb +103 -0
- metadata +408 -0
data/spec/config.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
test:
|
2
|
+
elascticseach_db:
|
3
|
+
driver: ElasticSearch
|
4
|
+
host: localhost
|
5
|
+
port: 9200
|
6
|
+
index: test
|
7
|
+
mongo_db:
|
8
|
+
driver: Mongo
|
9
|
+
host: localhost
|
10
|
+
port: 27017
|
11
|
+
collection: test
|
12
|
+
db:
|
13
|
+
driver: Mock::File
|
14
|
+
notifier:
|
15
|
+
driver: Mock::Web
|
16
|
+
logger: false
|
17
|
+
|
18
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/position', __FILE__)
|
3
|
+
|
4
|
+
include DCA::Mock
|
5
|
+
|
6
|
+
describe 'ElasticSearch storage' do
|
7
|
+
let(:connection) { @connection ||= DCA::ElasticSearchStorage.establish_connection APP_CONFIG[:elascticseach_db] }
|
8
|
+
let(:position) { ElasticSearchPosition.new :base_id => '0', :checksum => '0'}
|
9
|
+
let(:storage) { @storage ||= DCA::ElasticSearchStorage.new connection, position.class, :index => 'test' }
|
10
|
+
|
11
|
+
before :all do
|
12
|
+
connection
|
13
|
+
storage.index do
|
14
|
+
create
|
15
|
+
store :type => 'position', :base_id => '1', :checksum => '1'
|
16
|
+
refresh
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after :all do
|
21
|
+
storage.index do
|
22
|
+
delete
|
23
|
+
refresh
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it_behaves_like 'storage'
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Page</title>
|
4
|
+
</head>
|
5
|
+
<body>
|
6
|
+
<ul>
|
7
|
+
<li><a href="/positions/1">Position 1</a><span class="description">Description 1</span><span class="date">12.10.2012</span></li>
|
8
|
+
<li><a href="/positions/2">Position 2</a><span class="description">Description 2</span><span class="date">13.10.2012</span></li>
|
9
|
+
<li><a href="/positions/3">Position 3</a><span class="description">Description 3</span><span class="date">14.10.2012</span></li>
|
10
|
+
</ul>
|
11
|
+
</body>
|
12
|
+
</html>
|
data/spec/job_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require File.expand_path('../spec_helper', __FILE__)
|
3
|
+
|
4
|
+
module TestModule
|
5
|
+
class TestJob < DCA::Jobs::Job
|
6
|
+
end
|
7
|
+
|
8
|
+
class LoopJob < DCA::Jobs::Job
|
9
|
+
def perform
|
10
|
+
loop do
|
11
|
+
sleep 1
|
12
|
+
break if shutdown?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'Job' do
|
19
|
+
it 'should get queue name from module name' do
|
20
|
+
TestModule::TestJob.queue.should == 'TestModule'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should shutdown when QUIT signal is happened' do
|
24
|
+
job_pid = Process.fork { TestModule::LoopJob.create }
|
25
|
+
sleep 1
|
26
|
+
Process.kill 'QUIT', job_pid
|
27
|
+
Timeout::timeout(1) {
|
28
|
+
Process.waitpid job_pid
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module DCA
|
2
|
+
module Mock
|
3
|
+
class AnalyzerJob < DCA::Jobs::AnalyzerJob
|
4
|
+
|
5
|
+
def positions(&block)
|
6
|
+
positions = YAML.load_file(options[:fixture])
|
7
|
+
positions.each { |position| block.call Position.new position.symbolize_keys }
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch(position)
|
11
|
+
raise Exception if position.raise
|
12
|
+
position.failed ? nil : position
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Areas
|
18
|
+
module Mock
|
19
|
+
class AnalyzerJob < DCA::Jobs::AnalyzerJob
|
20
|
+
def perform
|
21
|
+
loop do
|
22
|
+
break if shutdown?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module DCA
|
2
|
+
module Mock
|
3
|
+
class FileStorage
|
4
|
+
|
5
|
+
attr_reader :collection
|
6
|
+
|
7
|
+
def initialize(connection, context, options = {})
|
8
|
+
@collection = DCA.project_name
|
9
|
+
@states = YAML.load_file('./spec/fixtures/states.yml')
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.establish_connection(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
def state(position)
|
16
|
+
@states[position.id].to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
def refresh(position, state)
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def context object
|
24
|
+
self.clone
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/mock/page.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module DCA
|
2
|
+
module Mock
|
3
|
+
class Page < DCA::Models::BaseModel
|
4
|
+
has_many :positions, ExtPosition, :selector => 'li'
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
class PageExt < DCA::Models::BaseModel
|
9
|
+
attr_accessor :category
|
10
|
+
has_many :positions, :selector => 'li', :polymorphic => :category, :append => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DCA
|
2
|
+
module Mock
|
3
|
+
class MongoSearchPosition < DCA::Models::Position
|
4
|
+
establish_connection :mongo_db
|
5
|
+
end
|
6
|
+
|
7
|
+
class ElasticSearchPosition < DCA::Models::Position
|
8
|
+
establish_connection :elascticseach_db
|
9
|
+
end
|
10
|
+
|
11
|
+
class Position < DCA::Models::Position
|
12
|
+
attr_accessor :raise, :failed, :title
|
13
|
+
|
14
|
+
has_one :base_id, :integer, :selector => 'a', :attribute => :href, :regex => /(\d+)$/
|
15
|
+
has_one :title, :string, :selector => 'a'
|
16
|
+
end
|
17
|
+
|
18
|
+
class ExtPosition < Position
|
19
|
+
has_one :description, :string, :selector => 'span.description'
|
20
|
+
has_one :date, :datetime, :selector => 'span.date'
|
21
|
+
end
|
22
|
+
|
23
|
+
class FullPosition < Position
|
24
|
+
has_one :base_id, :string, :selector => 'a'
|
25
|
+
end
|
26
|
+
|
27
|
+
class ChildPosition < DCA::Models::Position
|
28
|
+
attr_reader :name, :test
|
29
|
+
end
|
30
|
+
|
31
|
+
class RootPosition < DCA::Models::Position
|
32
|
+
has_one :one_child, :child_position
|
33
|
+
has_many :child_position
|
34
|
+
end
|
35
|
+
|
36
|
+
class PositionWithoutState < DCA::Models::BaseModel
|
37
|
+
attr_reader :name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module DCA
|
2
|
+
module Mock
|
3
|
+
class WebNotifier
|
4
|
+
def initialize config
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.queue
|
9
|
+
@queue ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.clean
|
13
|
+
@queue = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def push(object, event, options)
|
17
|
+
if event == :fetch && options[:result] == false
|
18
|
+
failed_queue = Mock::WebNotifier.queue[:failed] ||= {}
|
19
|
+
failed_queue[options[:state]] ||= 0
|
20
|
+
failed_queue[options[:state]] += 1
|
21
|
+
end
|
22
|
+
if [:analyze, :fetch].include? event
|
23
|
+
queue = Mock::WebNotifier.queue[event] ||= {}
|
24
|
+
queue[options[:state]] ||= 0
|
25
|
+
queue[options[:state]] += 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/position', __FILE__)
|
3
|
+
|
4
|
+
include DCA::Mock
|
5
|
+
|
6
|
+
describe DCA::MongoStorage do
|
7
|
+
let(:connection) { @connection ||= DCA::MongoStorage.establish_connection APP_CONFIG[:mongo_db] }
|
8
|
+
let(:position) { MongoSearchPosition.new :base_id => '0', :checksum => '0'}
|
9
|
+
let(:storage) { @storage ||= DCA::MongoStorage.new connection, position.class, :collection => 'test' }
|
10
|
+
|
11
|
+
before :all do
|
12
|
+
connection
|
13
|
+
end
|
14
|
+
|
15
|
+
after :all do
|
16
|
+
connection.drop_database storage.database.name
|
17
|
+
end
|
18
|
+
|
19
|
+
it_behaves_like 'storage'
|
20
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/notify_object', __FILE__)
|
3
|
+
require File.expand_path('../mock/position', __FILE__)
|
4
|
+
require File.expand_path('../mock/file_storage', __FILE__)
|
5
|
+
require File.expand_path('../mock/analyzer_job', __FILE__)
|
6
|
+
|
7
|
+
include DCA
|
8
|
+
|
9
|
+
describe 'Redis Notifier' do
|
10
|
+
|
11
|
+
it 'should connect to redis' do
|
12
|
+
DCA::Notifier.create :driver => 'Redis', :host => 'localhost', :port => '6379'
|
13
|
+
Ohm.redis.info
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'Instance' do
|
17
|
+
before :all do
|
18
|
+
DCA::Notifier.create :driver => 'Redis', :host => 'localhost', :port => '6379'
|
19
|
+
end
|
20
|
+
|
21
|
+
before :each do
|
22
|
+
DCA::Redis::Session.all.each { |session| session.delete }
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should push analyze notify' do
|
26
|
+
notify_object = Mock::NotifyObject.new
|
27
|
+
DCA::Notifier.push notify_object, :analyze, :state => :create
|
28
|
+
session = DCA::Redis::Session.find(:project => 'DCA', :area => 'test_queue', :uid => 'test_session').first
|
29
|
+
session.analyze_state(:create).count.should equal 1
|
30
|
+
|
31
|
+
DCA::Notifier.push notify_object, :analyze, :state => :create
|
32
|
+
session.analyze_state(:create).count.should equal 2
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should push analyze notify with different state' do
|
36
|
+
notify_object = Mock::NotifyObject.new
|
37
|
+
DCA::Notifier.push notify_object, :analyze, :state => :create
|
38
|
+
DCA::Notifier.push notify_object, :analyze, :state => :update
|
39
|
+
DCA::Notifier.push notify_object, :analyze, :state => :remove
|
40
|
+
DCA::Notifier.push notify_object, :analyze, :state => :unmodified
|
41
|
+
|
42
|
+
session = DCA::Redis::Session.find(:project => 'DCA', :area => 'test_queue', :uid => 'test_session').first
|
43
|
+
session.analyze_state(:create).count.should equal 1
|
44
|
+
session.analyze_state(:update).count.should equal 1
|
45
|
+
session.analyze_state(:remove).count.should equal 1
|
46
|
+
session.analyze_state(:unmodified).count.should equal 1
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should push fetch notify' do
|
50
|
+
notify_object = Mock::NotifyObject.new
|
51
|
+
DCA::Notifier.push notify_object, :fetch, :state => :create, :result => true
|
52
|
+
session = DCA::Redis::Session.find(:project => 'DCA', :area => 'test_queue', :uid => 'test_session').first
|
53
|
+
session.fetch_state(:create).success.should equal 1
|
54
|
+
|
55
|
+
DCA::Notifier.push notify_object, :fetch, :state => :create, :result => true
|
56
|
+
session.fetch_state(:create).success.should equal 2
|
57
|
+
|
58
|
+
DCA::Notifier.push notify_object, :fetch, :state => :create, :result => false
|
59
|
+
session.fetch_state(:create).failure.should equal 1
|
60
|
+
|
61
|
+
DCA::Notifier.push notify_object, :fetch, :state => :create, :result => false
|
62
|
+
session.fetch_state(:create).failure.should equal 2
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should push analyze notify with different state' do
|
66
|
+
notify_object = Mock::NotifyObject.new
|
67
|
+
DCA::Notifier.push notify_object, :fetch, :state => :create, :result => true
|
68
|
+
DCA::Notifier.push notify_object, :fetch, :state => :update, :result => true
|
69
|
+
|
70
|
+
session = DCA::Redis::Session.find(:project => 'DCA', :area => 'test_queue', :uid => 'test_session').first
|
71
|
+
session.fetch_state(:create).success.should equal 1
|
72
|
+
session.fetch_state(:update).success.should equal 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should notify failure' do
|
76
|
+
notify_object = Mock::NotifyObject.new
|
77
|
+
|
78
|
+
begin
|
79
|
+
raise "Test exception"
|
80
|
+
rescue Exception => exception
|
81
|
+
DCA::Notifier.push notify_object, :failure, :exception => exception
|
82
|
+
end
|
83
|
+
|
84
|
+
session = DCA::Redis::Session.find(:project => 'DCA', :area => 'test_queue', :uid => 'test_session').first
|
85
|
+
session.failures.count.should equal 1
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should work with analyze job' do
|
89
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
|
90
|
+
session = DCA::Redis::Session.all.first
|
91
|
+
session.analyze_state(:create).count.should equal 2
|
92
|
+
session.analyze_state(:unmodified).count.should equal 1
|
93
|
+
|
94
|
+
session.fetch_state(:create).success.should equal 1
|
95
|
+
session.fetch_state(:create).failure.should equal 1
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'yaml'
|
8
|
+
require 'rake'
|
9
|
+
require 'rspec'
|
10
|
+
require 'hashr'
|
11
|
+
|
12
|
+
SYS_ENV = 'test'
|
13
|
+
APP_CONFIG = YAML.load_file('./spec/config.yml')[SYS_ENV].deep_symbolize_keys
|
14
|
+
|
15
|
+
require './lib/dca'
|
16
|
+
|
17
|
+
|
18
|
+
# Set resque call perform method inline, without putting into redis queue
|
19
|
+
Resque.inline = true
|
20
|
+
|
21
|
+
# Requires supporting files with custom matchers and macros, etc,
|
22
|
+
# in ./support/ and its subdirectories.
|
23
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
24
|
+
|
25
|
+
RSpec.configure do |config|
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
shared_examples_for 'storage' do
|
2
|
+
context '.establish_connection' do
|
3
|
+
it 'should connect to storage' do
|
4
|
+
storage.should_not be_nil
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#state' do
|
9
|
+
context 'when new position' do
|
10
|
+
subject { position.state }
|
11
|
+
it { should equal :create}
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when modify position' do
|
15
|
+
before do
|
16
|
+
position.save
|
17
|
+
position.checksum = 1
|
18
|
+
end
|
19
|
+
|
20
|
+
after { position.destroy }
|
21
|
+
|
22
|
+
subject { position.state }
|
23
|
+
it { should equal :update}
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when exist position' do
|
27
|
+
before { position.save }
|
28
|
+
after { position.destroy }
|
29
|
+
subject { position.state }
|
30
|
+
it { should equal :unmodified}
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when position without state' do
|
34
|
+
let(:position) { PositionWithoutState.new name: 'test'}
|
35
|
+
subject { position.state }
|
36
|
+
it { should equal :create}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#refresh' do
|
41
|
+
def refresh state
|
42
|
+
storage.should_receive(state).with(position)
|
43
|
+
storage.refresh(position, state)
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when new position' do
|
47
|
+
it 'then create it' do
|
48
|
+
refresh :create
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when modify position' do
|
53
|
+
it 'then update it' do
|
54
|
+
refresh :update
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when old position' do
|
59
|
+
it 'then delete it' do
|
60
|
+
refresh :remove
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#create' do
|
66
|
+
before { position.save }
|
67
|
+
after { position.destroy }
|
68
|
+
|
69
|
+
it 'sould create position' do
|
70
|
+
storage.find(position).should_not be_nil
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'set position id' do
|
74
|
+
position.id.should_not be_nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#update' do
|
79
|
+
before do
|
80
|
+
position.save
|
81
|
+
position.checksum = '1'
|
82
|
+
position.save
|
83
|
+
end
|
84
|
+
after { position.destroy }
|
85
|
+
|
86
|
+
it 'should update position' do
|
87
|
+
storage.find(position)['checksum'].should eql '1'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#remove' do
|
92
|
+
before do
|
93
|
+
position.save
|
94
|
+
position.destroy
|
95
|
+
end
|
96
|
+
|
97
|
+
after { position.destroy }
|
98
|
+
|
99
|
+
it 'should remove position' do
|
100
|
+
storage.find(position).should be_nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|