riddl 0.99.105

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.
Files changed (92) hide show
  1. data/AUTHORS +1 -0
  2. data/COPYING +165 -0
  3. data/INSTALL +24 -0
  4. data/README.rdoc +2 -0
  5. data/Rakefile +17 -0
  6. data/TODO +17 -0
  7. data/contrib/riddl.jpg +0 -0
  8. data/contrib/riddl.png +0 -0
  9. data/contrib/riddl.svg +138 -0
  10. data/lib/riddl/client.rb +423 -0
  11. data/lib/riddl/commonlogger.rb +16 -0
  12. data/lib/riddl/constants.rb +5 -0
  13. data/lib/riddl/error.rb +8 -0
  14. data/lib/riddl/handlers.rb +14 -0
  15. data/lib/riddl/handlers/oauth.rb +19 -0
  16. data/lib/riddl/handlers/plain-type.rb +21 -0
  17. data/lib/riddl/handlers/relaxng.rb +16 -0
  18. data/lib/riddl/handlers/xmlschema.rb +16 -0
  19. data/lib/riddl/header.rb +9 -0
  20. data/lib/riddl/implementation.rb +57 -0
  21. data/lib/riddl/ns/common-patterns/addon-security/request.xml +25 -0
  22. data/lib/riddl/ns/common-patterns/addon-security/response.xml +25 -0
  23. data/lib/riddl/ns/common-patterns/downloadify/1.0/downloadify.xml +18 -0
  24. data/lib/riddl/ns/common-patterns/notifications-consumer/1.0/consumer.xml +100 -0
  25. data/lib/riddl/ns/common-patterns/notifications-producer/1.0/producer.xml +204 -0
  26. data/lib/riddl/ns/common-patterns/properties/1.0/properties.schema.schema +140 -0
  27. data/lib/riddl/ns/common-patterns/properties/1.0/properties.schema.xsl +89 -0
  28. data/lib/riddl/ns/common-patterns/properties/1.0/properties.xml +150 -0
  29. data/lib/riddl/ns/common/datatypes-1_0.rng +79 -0
  30. data/lib/riddl/ns/common/relaxng-modular.rng +330 -0
  31. data/lib/riddl/ns/common/relaxng.rng +10 -0
  32. data/lib/riddl/ns/declaration/1.0/declaration.rng +114 -0
  33. data/lib/riddl/ns/description/1.0/description.rng +302 -0
  34. data/lib/riddl/option.rb +9 -0
  35. data/lib/riddl/parameter.rb +54 -0
  36. data/lib/riddl/protocols/http/generator.rb +121 -0
  37. data/lib/riddl/protocols/http/parser.rb +199 -0
  38. data/lib/riddl/protocols/websocket.rb +103 -0
  39. data/lib/riddl/protocols/xmpp/generator.rb +176 -0
  40. data/lib/riddl/protocols/xmpp/parser.rb +118 -0
  41. data/lib/riddl/roles.rb +15 -0
  42. data/lib/riddl/roles/http%3A%2F%2Foauth.net%2F1.0%2Faccess_token.rb +30 -0
  43. data/lib/riddl/roles/http%3A%2F%2Foauth.net%2F1.0%2Fon_behalf.rb +22 -0
  44. data/lib/riddl/roles/http%3A%2F%2Foauth.net%2F1.0%2Frequest_token.rb +30 -0
  45. data/lib/riddl/roles/http%3A%2F%2Foauth.net%2F1.0/base.rb +67 -0
  46. data/lib/riddl/server.rb +519 -0
  47. data/lib/riddl/utils/description.rb +29 -0
  48. data/lib/riddl/utils/downloadify.rb +14 -0
  49. data/lib/riddl/utils/erbserve.rb +23 -0
  50. data/lib/riddl/utils/fileserve.rb +31 -0
  51. data/lib/riddl/utils/notifications_producer.rb +310 -0
  52. data/lib/riddl/utils/properties.rb +474 -0
  53. data/lib/riddl/utils/xsloverlay.rb +21 -0
  54. data/lib/riddl/wrapper.rb +280 -0
  55. data/lib/riddl/wrapper/declaration.rb +103 -0
  56. data/lib/riddl/wrapper/declaration/facade.rb +94 -0
  57. data/lib/riddl/wrapper/declaration/interface.rb +34 -0
  58. data/lib/riddl/wrapper/declaration/tile.rb +107 -0
  59. data/lib/riddl/wrapper/description.rb +69 -0
  60. data/lib/riddl/wrapper/description/access.rb +108 -0
  61. data/lib/riddl/wrapper/description/message_and_transformation.rb +131 -0
  62. data/lib/riddl/wrapper/description/resource.rb +271 -0
  63. data/lib/riddl/wrapper/layerchecker.rb +33 -0
  64. data/lib/riddl/wrapper/messageparser.rb +221 -0
  65. data/lib/riddl/wrapper/resourcechecker.rb +98 -0
  66. data/ns/common-patterns/addon-security/request.xml +25 -0
  67. data/ns/common-patterns/addon-security/response.xml +25 -0
  68. data/ns/common-patterns/downloadify/1.0/downloadify.xml +18 -0
  69. data/ns/common-patterns/notifications-consumer/1.0/consumer.xml +100 -0
  70. data/ns/common-patterns/notifications-producer/1.0/producer.xml +204 -0
  71. data/ns/common-patterns/properties/1.0/properties.schema.schema +140 -0
  72. data/ns/common-patterns/properties/1.0/properties.schema.xsl +89 -0
  73. data/ns/common-patterns/properties/1.0/properties.xml +150 -0
  74. data/ns/common/datatypes-1_0.rng +79 -0
  75. data/ns/common/relaxng-modular.rng +330 -0
  76. data/ns/common/relaxng.rng +10 -0
  77. data/ns/declaration/1.0/declaration.rng +114 -0
  78. data/ns/description/1.0/description.rng +302 -0
  79. data/riddl.gemspec +33 -0
  80. data/test/smartrunner.rb +48 -0
  81. data/test/tc_declaration-distributed.rb +79 -0
  82. data/test/tc_declaration-hybrid.rb +71 -0
  83. data/test/tc_declaration-local.rb +47 -0
  84. data/test/tc_helloworld.rb +17 -0
  85. data/test/tc_producer.rb +54 -0
  86. data/test/tc_properties.rb +72 -0
  87. data/tools/flash-policy-server.rb +12 -0
  88. data/tools/riddlcheck +36 -0
  89. data/tools/riddlcheck-1_0 +36 -0
  90. data/tools/riddlprocess +51 -0
  91. data/tools/riddlprocess-1_0 +51 -0
  92. metadata +291 -0
@@ -0,0 +1,199 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../constants')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../../parameter')
3
+
4
+ module Riddl
5
+ module Protocols
6
+ module HTTP
7
+ class Parser
8
+ MULTIPART_CONTENT_TYPES = [
9
+ #{{{
10
+ 'multipart/form-data',
11
+ 'multipart/related',
12
+ 'multipart/mixed'
13
+ #}}}
14
+ ].freeze
15
+ FORM_CONTENT_TYPES = [
16
+ #{{{
17
+ nil,
18
+ 'application/x-www-form-urlencoded'
19
+ #}}}
20
+ ].freeze
21
+
22
+ def self::unescape(s)
23
+ #{{{
24
+ return s if s.nil?
25
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
26
+ [$1.delete('%')].pack('H*')
27
+ }
28
+ #}}}
29
+ end
30
+
31
+ def parse_content(input,ctype,content_length,content_disposition,content_id,riddl_type)
32
+ #{{{
33
+ # fixing for chunked?
34
+
35
+ ctype = nil if riddl_type == 'simple'
36
+ filename = content_disposition[/ filename="?([^\";]*)"?/ni, 1]
37
+ name = content_disposition[/ name="?([^\";]*)"?/ni, 1] || content_id
38
+
39
+ if ctype || filename
40
+ body = Parameter::Tempfile.new("RiddlMultipart")
41
+ body.binmode if body.respond_to?(:binmode)
42
+ else
43
+ body = ''
44
+ end
45
+
46
+ if content_length == 0 && name == ''
47
+ body << input.read
48
+ end
49
+
50
+ bufsize = 16384
51
+ until content_length <= 0
52
+ c = input.read(bufsize < content_length ? bufsize : content_length)
53
+ raise EOFError, "bad content body" if c.nil? || c.empty?
54
+ body << c
55
+ content_length -= c.size
56
+ end
57
+ body.rewind if body.respond_to?(:binmode)
58
+
59
+ add_to_params(name,body,filename,ctype,nil)
60
+ #}}}
61
+ end
62
+ private :parse_content
63
+
64
+ def parse_multipart(input,content_type,content_length)
65
+ #{{{
66
+ content_type =~ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
67
+ boundary = "--#{$1}"
68
+
69
+ boundary_size = boundary.size + EOL.size
70
+ content_length -= boundary_size
71
+ status = input.read(boundary_size)
72
+ raise EOFError, "bad content body" unless status == boundary + EOL
73
+
74
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
75
+
76
+ buf = ""
77
+ bufsize = 16384
78
+ loop do
79
+ head = nil
80
+ body = ''
81
+ filename = ctype = name = nil
82
+
83
+ until head && buf =~ rx
84
+ if !head && i = buf.index(EOL+EOL)
85
+ head = buf.slice!(0, i+2) # First \r\n
86
+ buf.slice!(0, 2) # Second \r\n
87
+
88
+ filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
89
+ ctype = head[/Content-Type: (.*)#{EOL}/ni, 1]
90
+ name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
91
+
92
+ if ctype || filename
93
+ body = Parameter::Tempfile.new("RiddlMultipart")
94
+ body.binmode if body.respond_to?(:binmode)
95
+ end
96
+
97
+ next
98
+ end
99
+
100
+ # Save the read body part.
101
+ if head && (boundary_size+4 < buf.size)
102
+ body << buf.slice!(0, buf.size - (boundary_size+4))
103
+ end
104
+
105
+ c = input.read(bufsize < content_length ? bufsize : content_length)
106
+ raise EOFError, "bad content body" if c.nil? || c.empty?
107
+ content_length -= c.size
108
+ buf << c
109
+ end
110
+
111
+ # Save the rest.
112
+ if i = buf.index(rx)
113
+ body << buf.slice!(0, i)
114
+ buf.slice!(0, boundary_size+2)
115
+ content_length = -1 if $1 == "--"
116
+ end
117
+
118
+ add_to_params(name,body,filename,ctype,head)
119
+
120
+ break if buf.empty? || content_length == -1
121
+ end
122
+ #}}}
123
+ end
124
+ private :parse_multipart
125
+
126
+ def add_to_params(name,body,filename,ctype,head)
127
+ #{{{
128
+ if filename == ""
129
+ # filename is blank which means no file has been selected
130
+ elsif filename && ctype
131
+ body.rewind
132
+
133
+ # Take the basename of the upload's original filename.
134
+ # This handles the full Windows paths given by Internet Explorer
135
+ # (and perhaps other broken user agents) without affecting
136
+ # those which give the lone filename.
137
+ filename =~ /^(?:.*[:\\\/])?(.*)/m
138
+ filename = $1
139
+
140
+ @params << Parameter::Complex.new(name, ctype, body, filename, head)
141
+ elsif !filename && ctype
142
+ body.rewind
143
+
144
+ # Generic multipart cases, not coming from a form
145
+ @params << Parameter::Complex.new(name, ctype, body, nil, head)
146
+ else
147
+ @params << Parameter::Simple.new(name, body, :body)
148
+ end
149
+ #}}}
150
+ end
151
+ private :add_to_params
152
+
153
+ def parse_nested_query(qs, type)
154
+ #{{{
155
+ (qs || '').split(/[#{D}] */n).each do |p|
156
+ k, v = self.class::unescape(p).split('=', 2)
157
+ @params << Parameter::Simple.new(k,v,type)
158
+ end
159
+ #}}}
160
+ end
161
+ private :parse_nested_query
162
+
163
+ def initialize(query_string,input,content_type,content_length,content_disposition,content_id,riddl_type)
164
+ #{{{
165
+ # rewind because in some cases it is not at start (when multipart without length)
166
+
167
+ begin
168
+ input.rewind if input.respond_to?(:rewind)
169
+ rescue Errno::ESPIPE
170
+ # Handles exceptions raised by input streams that cannot be rewound
171
+ # such as when using plain CGI under Apache
172
+ end
173
+
174
+ media_type = content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
175
+ @params = Riddl::Parameter::Array.new
176
+ parse_nested_query(query_string,:query)
177
+ if MULTIPART_CONTENT_TYPES.include?(media_type)
178
+ parse_multipart(input,content_type,content_length.to_i)
179
+ elsif FORM_CONTENT_TYPES.include?(media_type)
180
+ # sub is a fix for Safari Ajax postings that always append \0
181
+ parse_nested_query(input.read.sub(/\0\z/, ''),:body)
182
+ else
183
+ parse_content(input,content_type,content_length.to_i,content_disposition||'',content_id||'',riddl_type||'')
184
+ end
185
+
186
+ begin
187
+ input.rewind if input.respond_to?(:rewind)
188
+ rescue Errno::ESPIPE
189
+ # Handles exceptions raised by input streams that cannot be rewound
190
+ # such as when using plain CGI under Apache
191
+ end
192
+ #}}}
193
+ end
194
+
195
+ attr_reader :params
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,103 @@
1
+ gem 'em-websocket', '>= 0.4.0'
2
+ require 'em-websocket'
3
+ require 'thin'
4
+
5
+ module Thin #{{{
6
+ class Connection
7
+ attr_accessor :websocket
8
+ def websocket?
9
+ !self.websocket.nil?
10
+ end
11
+
12
+ def pre_process_with_websocket
13
+ @request.env['thin.connection'] = self
14
+ pre_process_without_websocket
15
+ end
16
+ alias :pre_process_without_websocket :pre_process
17
+ alias :pre_process :pre_process_with_websocket
18
+
19
+ def receive_data_with_websocket(data)
20
+ if self.websocket?
21
+ self.websocket.receive_data(data)
22
+ else
23
+ receive_data_without_websocket(data)
24
+ end
25
+ end
26
+ alias :receive_data_without_websocket :receive_data
27
+ alias :receive_data :receive_data_with_websocket
28
+
29
+ def unbind_with_websocket
30
+ if self.websocket?
31
+ self.websocket.unbind
32
+ else
33
+ unbind_without_websocket
34
+ end
35
+ end
36
+ alias :unbind_without_websocket :unbind
37
+ alias :unbind :unbind_with_websocket
38
+ end
39
+ end #}}}
40
+
41
+ class WebSocketParserData
42
+ attr_accessor :headers, :request_path, :query_string, :http_method, :body, :request_url
43
+ def match(what)
44
+ @body =~ what
45
+ end
46
+ def upgrade?
47
+ true
48
+ end
49
+ end
50
+
51
+ module EventMachine
52
+ module WebSocket
53
+ class Handshake
54
+ def receive_data(data)
55
+ @parser = data
56
+ @headers = data.headers
57
+ process(@headers, data.body)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ module Riddl
64
+ class WebSocket < ::EventMachine::WebSocket::Connection
65
+ class Error < RuntimeError; end
66
+
67
+ def self.new(*args)
68
+ instance = allocate
69
+ instance.__send__(:initialize, *args)
70
+ instance
71
+ end
72
+
73
+ def send_data(*args)
74
+ EM.next_tick do
75
+ @socket.send_data(*args)
76
+ end
77
+ end
78
+
79
+ def close_connection(*args)
80
+ EM.next_tick do
81
+ @socket.close_connection(*args)
82
+ end
83
+ end
84
+
85
+ def trigger_on_message(msg); @app.onmessage(msg); end
86
+ def trigger_on_open(handshake); @closed = false; @app.onopen; end
87
+ def trigger_on_close; @closed = true; @app.onclose; end
88
+ def trigger_on_error(error); @closed = true; @app.onerror(error); true; end
89
+
90
+ def initialize(app, socket)
91
+ @app = app
92
+ @socket = socket
93
+ @ssl = socket.backend.respond_to?(:ssl?) && socket.backend.ssl?
94
+ @closed = true
95
+ socket.websocket = self
96
+ socket.comm_inactivity_timeout = 0
97
+ end
98
+
99
+ def closed?
100
+ @closed
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,176 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../constants')
2
+ require 'blather/client/client'
3
+ require 'securerandom'
4
+
5
+ module Riddl
6
+ module Protocols
7
+ module XMPP
8
+ XR_NS = 'http://www.fp7-adventure.eu/ns/xmpp-rest'.freeze
9
+
10
+ class Stanza < Blather::Stanza
11
+ def self.new
12
+ node = super :message
13
+ node.type = :normal
14
+ node.id = SecureRandom.uuid
15
+ node
16
+ end
17
+ end
18
+
19
+ class Error
20
+ MAPPING = {
21
+ 302 => ['redirect' , 'modify' , '302 Redirect' ],
22
+ 400 => ['bad-request' , 'modify' , '400 Bad Request' ],
23
+ 401 => ['not-authorized' , 'auth' , '401 Not Authorized ' ],
24
+ 402 => ['payment-required' , 'auth' , '402 Payment Required' ],
25
+ 403 => ['forbidden' , 'auth' , '403 Forbidden' ],
26
+ 404 => ['item-not-found' , 'cancel' , '404 Not Found' ],
27
+ 405 => ['not-allowed' , 'cancel' , '405 Not Allowed' ],
28
+ 406 => ['not-acceptable' , 'modify' , '406 Not Acceptable' ],
29
+ 407 => ['registration-required' , 'auth' , '407 Registration Required' ],
30
+ 408 => ['remote-server-timeout' , 'wait' , '408 Request Timeout' ],
31
+ 409 => ['conflict' , 'cancel' , '409 Conflict' ],
32
+ 500 => ['internal-server-error' , 'wait' , '500 Internal Server Error' ],
33
+ 501 => ['feature-not-implemented' , 'cancel' , '501 Not Implemented' ],
34
+ 502 => ['service-unavailable' , 'wait' , '502 Remote Server Error' ],
35
+ 503 => ['service-unavailable' , 'cancel' , '503 Service Unavailable' ],
36
+ 504 => ['remote-server-timeout' , 'wait' , '504 Remote Server Timeout' ],
37
+ 510 => ['service-unavailable' , 'cancel' , '510 Disconnected' ]
38
+ }.freeze
39
+ UNDEFINED = [
40
+ 'undefined-condition', 'modify'
41
+ ].freeze
42
+
43
+ def initialize(err)
44
+ m = Stanza.new
45
+ @stanza = if MAPPING[err]
46
+ Blather::StanzaError.new(m,*MAPPING[err]).to_node
47
+ else
48
+ Blather::StanzaError.new(m,*UNDEFINED,'#{err} see http://www.iana.org/assignments/http-status-codes/http-status-codes.xml').to_node
49
+ end
50
+ end
51
+
52
+ def generate
53
+ @stanza
54
+ end
55
+ end
56
+
57
+ class Generator
58
+ def initialize(what,params,headers)
59
+ @params = params
60
+ @stanza = Stanza.new
61
+ @node = XML::Smart::Dom::Element.new(@stanza)
62
+ if what.is_a?(Fixnum)
63
+ @node.add('ok').namespaces.add(nil,XR_NS)
64
+ else
65
+ @node.add('operation',what).namespaces.add(nil,XR_NS)
66
+ end
67
+ headers.each do |k,v|
68
+ @node.add('header',v,:name => k).namespaces.add(nil,XR_NS)
69
+ end
70
+ end
71
+
72
+ # Performs URI escaping so that you can construct proper
73
+ # query strings faster. Use this rather than the cgi.rb
74
+ # version since it's faster. (%20 instead of + for improved standards conformance).
75
+ def self.escape(s)
76
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
77
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
78
+ }
79
+ end
80
+
81
+ def generate(mode=:output)
82
+ if @params.is_a?(Array) && @params.length == 1
83
+ body(@params[0],mode)
84
+ elsif @params.class == Riddl::Parameter::Simple || @params.class == Riddl::Parameter::Complex
85
+ body(@params,mode)
86
+ elsif @params.is_a?(Array) && @params.length > 1
87
+ multipart(mode)
88
+ else
89
+ @stanza
90
+ end
91
+ end
92
+
93
+ def body(r,mode)
94
+ case r
95
+ when Riddl::Parameter::Simple
96
+ if mode == :output
97
+ n = @node.add('part',r.value)
98
+ n.namespaces.add(nil,XR_NS)
99
+ n.attributes['content-type'] = 'text/plain'
100
+ n.attributes['content-id'] = r.name
101
+ n.attributes['RIDDL-TYPE'] = 'simple'
102
+ end
103
+ if mode == :input
104
+ n = @node.add('part')
105
+ n.namespaces.add(nil,XR_NS)
106
+ n.attributes['content-type'] = 'application/x-www-form-urlencoded'
107
+ n.text = self.class::escape(r.name) + '=' + self.class::escape(r.value)
108
+ end
109
+ when Riddl::Parameter::Complex
110
+ n = @node.add('part')
111
+ n.namespaces.add(nil,XR_NS)
112
+ n.text = (r.value.respond_to?(:read) ? r.value.read : r.value)
113
+ n.attributes['content-type'] = r.mimetype
114
+ n.attributes['RIDDL-TYPE'] = 'complex'
115
+ if r.filename.nil?
116
+ n.attributes['content-id'] = r.name
117
+ else
118
+ n.attributes['content-disposition'] = "riddl-data; name=\"#{r.name}\"; filename=\"#{r.filename}\""
119
+ end
120
+ end
121
+ @stanza
122
+ end
123
+ private :body
124
+
125
+ def multipart(mode)
126
+ scount = ccount = 0
127
+ @params.each do |r|
128
+ case r
129
+ when Riddl::Parameter::Simple
130
+ scount += 1
131
+ when Riddl::Parameter::Complex
132
+ ccount += 1
133
+ end
134
+ end
135
+ if scount > 0 && ccount == 0
136
+ n = @node.add('part')
137
+ n.namespaces.add(nil,XR_NS)
138
+ n.attributes['content-type'] = 'application/x-www-form-urlencoded'
139
+ res = []
140
+ @params.each do |r|
141
+ case r
142
+ when Riddl::Parameter::Simple
143
+ res << self.class::escape(r.name) + '=' + self.class::escape(r.value)
144
+ end
145
+ end
146
+ n.text = res.join('&')
147
+ else
148
+ if scount + ccount > 0
149
+ @params.each do |r|
150
+ case r
151
+ when Riddl::Parameter::Simple
152
+ n = @node.add('part')
153
+ n.namespaces.add(nil,XR_NS)
154
+ n.attributes['RIDDL-TYPE'] = 'simple'
155
+ n.attributes['content-disposition'] = "#{mode == :input ? 'form-data' : 'riddl-data'}; name=\"#{r.name}\""
156
+ n.text = r.value
157
+ when Riddl::Parameter::Complex
158
+ n = @node.add('part')
159
+ n.namespaces.add(nil,XR_NS)
160
+ n.attributes['RIDDL-TYPE'] = 'complex'
161
+ n.attributes['content-disposition'] = "#{mode == :input ? 'form-data' : 'riddl-data'}; name=\"#{r.name}\"#{r.filename.nil? ? '' : "; filename=\"#{r.filename}\""}"
162
+ n.attributes['content-transfer-encoding'] = 'binary'
163
+ n.attributes['content-type'] = r.mimetype
164
+ n.text = (r.value.respond_to?(:read) ? r.value.read : r.value)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ @stanza
170
+ end
171
+ private :multipart
172
+
173
+ end
174
+ end
175
+ end
176
+ end