mongrel2 0.0.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.
Files changed (70) hide show
  1. data.tar.gz.sig +0 -0
  2. data/.gemtest +0 -0
  3. data/History.rdoc +4 -0
  4. data/Manifest.txt +66 -0
  5. data/README.rdoc +169 -0
  6. data/Rakefile +77 -0
  7. data/bin/m2sh.rb +600 -0
  8. data/data/mongrel2/bootstrap.html +25 -0
  9. data/data/mongrel2/config.sql +84 -0
  10. data/data/mongrel2/mimetypes.sql +855 -0
  11. data/examples/README.txt +6 -0
  12. data/examples/config.rb +54 -0
  13. data/examples/helloworld-handler.rb +31 -0
  14. data/examples/request-dumper.rb +45 -0
  15. data/examples/request-dumper.tmpl +71 -0
  16. data/examples/run +17 -0
  17. data/lib/mongrel2.rb +62 -0
  18. data/lib/mongrel2/config.rb +212 -0
  19. data/lib/mongrel2/config/directory.rb +78 -0
  20. data/lib/mongrel2/config/dsl.rb +206 -0
  21. data/lib/mongrel2/config/handler.rb +124 -0
  22. data/lib/mongrel2/config/host.rb +88 -0
  23. data/lib/mongrel2/config/log.rb +48 -0
  24. data/lib/mongrel2/config/mimetype.rb +15 -0
  25. data/lib/mongrel2/config/proxy.rb +15 -0
  26. data/lib/mongrel2/config/route.rb +51 -0
  27. data/lib/mongrel2/config/server.rb +58 -0
  28. data/lib/mongrel2/config/setting.rb +15 -0
  29. data/lib/mongrel2/config/statistic.rb +23 -0
  30. data/lib/mongrel2/connection.rb +212 -0
  31. data/lib/mongrel2/constants.rb +159 -0
  32. data/lib/mongrel2/control.rb +165 -0
  33. data/lib/mongrel2/exceptions.rb +59 -0
  34. data/lib/mongrel2/handler.rb +309 -0
  35. data/lib/mongrel2/httprequest.rb +51 -0
  36. data/lib/mongrel2/httpresponse.rb +187 -0
  37. data/lib/mongrel2/jsonrequest.rb +43 -0
  38. data/lib/mongrel2/logging.rb +241 -0
  39. data/lib/mongrel2/mixins.rb +143 -0
  40. data/lib/mongrel2/request.rb +148 -0
  41. data/lib/mongrel2/response.rb +74 -0
  42. data/lib/mongrel2/table.rb +216 -0
  43. data/lib/mongrel2/xmlrequest.rb +36 -0
  44. data/spec/lib/constants.rb +237 -0
  45. data/spec/lib/helpers.rb +246 -0
  46. data/spec/lib/matchers.rb +50 -0
  47. data/spec/mongrel2/config/directory_spec.rb +91 -0
  48. data/spec/mongrel2/config/dsl_spec.rb +218 -0
  49. data/spec/mongrel2/config/handler_spec.rb +118 -0
  50. data/spec/mongrel2/config/host_spec.rb +30 -0
  51. data/spec/mongrel2/config/log_spec.rb +95 -0
  52. data/spec/mongrel2/config/proxy_spec.rb +30 -0
  53. data/spec/mongrel2/config/route_spec.rb +83 -0
  54. data/spec/mongrel2/config/server_spec.rb +84 -0
  55. data/spec/mongrel2/config/setting_spec.rb +30 -0
  56. data/spec/mongrel2/config/statistic_spec.rb +30 -0
  57. data/spec/mongrel2/config_spec.rb +111 -0
  58. data/spec/mongrel2/connection_spec.rb +172 -0
  59. data/spec/mongrel2/constants_spec.rb +32 -0
  60. data/spec/mongrel2/control_spec.rb +192 -0
  61. data/spec/mongrel2/handler_spec.rb +261 -0
  62. data/spec/mongrel2/httpresponse_spec.rb +232 -0
  63. data/spec/mongrel2/logging_spec.rb +76 -0
  64. data/spec/mongrel2/mixins_spec.rb +62 -0
  65. data/spec/mongrel2/request_spec.rb +157 -0
  66. data/spec/mongrel2/response_spec.rb +81 -0
  67. data/spec/mongrel2/table_spec.rb +176 -0
  68. data/spec/mongrel2_spec.rb +34 -0
  69. metadata +294 -0
  70. metadata.gz.sig +0 -0
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'nokogiri'
4
+
5
+ require 'mongrel2/request' unless defined?( Mongrel2::Request )
6
+ require 'mongrel2/mixins'
7
+
8
+
9
+ # The Mongrel2 XML Request class. Instances of this class represent a request for an XML route from
10
+ # a Mongrel2 server.
11
+ class Mongrel2::XMLRequest < Mongrel2::Request
12
+ include Mongrel2::Loggable
13
+
14
+ register_request_type( self, :XML )
15
+
16
+
17
+ ### Parse the body as JSON.
18
+ def initialize( sender_id, conn_id, path, headers, body, raw=nil )
19
+ super
20
+ self.log.debug "Parsing XML request body"
21
+ @data = Nokogiri::XML( body )
22
+ end
23
+
24
+
25
+ ######
26
+ public
27
+ ######
28
+
29
+ # The parsed request data (a Nokogiri::XML document)
30
+ attr_reader :data
31
+
32
+
33
+ end # class Mongrel2::XMLRequest
34
+
35
+ # vim: set nosta noet ts=4 sw=4:
36
+
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'uri'
4
+ require 'yajl'
5
+ require 'tnetstring'
6
+
7
+ require 'mongrel2' unless defined?( Mongrel2 )
8
+
9
+
10
+ ### A collection of constants used in testing
11
+ module Mongrel2::TestConstants # :nodoc:all
12
+
13
+ include Mongrel2::Constants
14
+
15
+ unless defined?( TEST_HOST )
16
+
17
+ TEST_HOST = 'localhost'
18
+ TEST_PORT = 8118
19
+
20
+ # Rule 2: Every message to and from Mongrel2 has that Mongrel2 instances
21
+ # UUID as the very first thing.
22
+ TEST_UUID = 'BD17D85C-4730-4BF2-999D-9D2B2E0FCCF9'
23
+
24
+ # 0mq socket specifications for Handlers
25
+ TEST_SEND_SPEC = 'tcp://127.0.0.1:9998'
26
+ TEST_RECV_SPEC = 'tcp://127.0.0.1:9997'
27
+
28
+ # Rule 3: Mongrel2 sends requests with one number right after the
29
+ # servers UUID separated by a space. Handlers return a netstring with
30
+ # a list of numbers separated by spaces. The numbers indicate the
31
+ # connected browser the message is to/from.
32
+ TEST_ID = 8
33
+
34
+ #
35
+ # HTTP request constants
36
+ #
37
+
38
+ TEST_ROUTE = '/handler'
39
+ TEST_PATH = TEST_ROUTE
40
+ TEST_QUERY = 'thing=foom'
41
+
42
+ TEST_HEADERS = {
43
+ "x-forwarded-for" => "127.0.0.1",
44
+ "accept-language" => "en-US,en;q=0.8",
45
+ "accept-encoding" => "gzip,deflate,sdch",
46
+ "connection" => "keep-alive",
47
+ "accept-charset" => "UTF-8,*;q=0.5",
48
+ "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
49
+ "user-agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) " +
50
+ "AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 " +
51
+ "Safari/535.1",
52
+ "host" => "localhost:3667",
53
+ "METHOD" => "GET",
54
+ "VERSION" => "HTTP/1.1",
55
+ }
56
+
57
+ TEST_BODY = ''
58
+
59
+ TEST_REQUEST_OPTS = {
60
+ :uuid => TEST_UUID,
61
+ :id => TEST_ID,
62
+ :path => TEST_PATH,
63
+ :body => TEST_BODY,
64
+ }
65
+
66
+
67
+ #
68
+ # JSON (JSSocket, etc.) request constants
69
+ #
70
+
71
+ TEST_JSON_PATH = '@directory'
72
+
73
+ TEST_JSON_HEADERS = {
74
+ 'PATH' => TEST_JSON_PATH,
75
+ 'x-forwarded-for' => "127.0.0.1",
76
+ 'METHOD' => "JSON",
77
+ 'PATTERN' => TEST_JSON_PATH,
78
+ }
79
+ TEST_JSON_BODY = { 'type' => 'msg', 'msg' => 'connect' }
80
+
81
+ TEST_JSON_REQUEST_OPTS = {
82
+ :uuid => TEST_UUID,
83
+ :id => TEST_ID,
84
+ :path => TEST_JSON_PATH,
85
+ :body => TEST_JSON_BODY,
86
+ }
87
+
88
+
89
+ #
90
+ # XML message request constants
91
+ #
92
+
93
+ TEST_XML_PATH = '<directory'
94
+
95
+ TEST_XML_HEADERS = {
96
+ 'PATH' => TEST_XML_PATH,
97
+ 'x-forwarded-for' => "127.0.0.1",
98
+ 'METHOD' => "XML",
99
+ 'PATTERN' => TEST_XML_PATH,
100
+ }
101
+ TEST_XML_BODY = '<directory><file name="foom.txt" /><file name="foom2.md" /></directory>'
102
+
103
+ TEST_XML_REQUEST_OPTS = {
104
+ :uuid => TEST_UUID,
105
+ :id => TEST_ID,
106
+ :path => TEST_XML_PATH,
107
+ :body => TEST_XML_BODY,
108
+ }
109
+
110
+
111
+ #
112
+ # HTTP constants
113
+ #
114
+
115
+ # Space
116
+ SP = '\\x20'
117
+
118
+ # Network EOL
119
+ CRLF = '\\r\\n'
120
+
121
+ # Pattern to match the contents of ETag and If-None-Match headers
122
+ ENTITY_TAG_PATTERN = %r{
123
+ (w/)? # Weak flag
124
+ " # Opaque-tag
125
+ ([^"]+) # Quoted-string
126
+ " # Closing quote
127
+ }ix
128
+
129
+ # Separators = "(" | ")" | "<" | ">" | "@"
130
+ # | "," | ";" | ":" | "\" | <">
131
+ # | "/" | "[" | "]" | "?" | "="
132
+ # | "{" | "}" | SP | HT
133
+ SEPARATORS = Regexp.quote("(\")<>@,;:\\/[]?={} \t")
134
+
135
+ # token = 1*<any CHAR except CTLs or separators>
136
+ TOKEN = /[^#{SEPARATORS}[:cntrl:]]+/
137
+
138
+ # Borrow URI's pattern for matching absolute URIs
139
+ REQUEST_URI = URI::REL_URI_REF
140
+
141
+ # Canonical HTTP methods
142
+ REQUEST_METHOD = /OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT/
143
+
144
+ # Extension HTTP methods
145
+ # extension-method = token
146
+ EXTENSION_METHOD = TOKEN
147
+
148
+ # HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT
149
+ HTTP_VERSION = %r{HTTP/(\d+\.\d+)}
150
+
151
+ # LWS = [CRLF] 1*( SP | HT )
152
+ LWS = /#{CRLF}[ \t]+/
153
+
154
+ # TEX = <any OCTET except CTLs, but including LWS>
155
+ TEXT = /[^[:cntrl:]]|#{LWS}/
156
+
157
+ # Reason-Phrase = *<TEXT, excluding CR, LF>
158
+ REASON_PHRASE = %r{[^[:cntrl:]]+}
159
+
160
+ # Pattern to match HTTP response lines
161
+ # Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
162
+ HTTP_RESPONSE_LINE = %r{
163
+ (?<http_version>#{HTTP_VERSION})
164
+ #{SP}
165
+ (?<status_code>\d{3})
166
+ #{SP}
167
+ (?<reason_phrase>#{REASON_PHRASE})
168
+ #{CRLF}
169
+ }x
170
+
171
+ # message-header = field-name ":" [ field-value ]
172
+ # field-name = token
173
+ # field-value = *( field-content | LWS )
174
+ # field-content = <the OCTETs making up the field-value
175
+ # and consisting of either *TEXT or combinations
176
+ # of token, separators, and quoted-string>
177
+
178
+ # Pattern to match a single header tuple, possibly split over multiple lines
179
+ HEADER_LINE = %r{
180
+ ^
181
+ #{TOKEN}
182
+ :
183
+ (?:#{LWS}|#{TEXT})*
184
+ #{CRLF}
185
+ }mx
186
+
187
+ # entity-body = *OCTET
188
+ MESSAGE_BODY = /.*/
189
+
190
+ # Pattern to match an entire HTTP response
191
+ # Response = Status-Line ; Section 6.1
192
+ # *(( general-header ; Section 4.5
193
+ # | response-header ; Section 6.2
194
+ # | entity-header ) CRLF) ; Section 7.1
195
+ # CRLF
196
+ # [ message-body ] ; Section 7.2
197
+ HTTP_RESPONSE = %r{
198
+ ^
199
+ (?<response_line>#{HTTP_RESPONSE_LINE})
200
+ (?<headers>#{HEADER_LINE}*)
201
+ #{CRLF}
202
+ (?<message_body>#{MESSAGE_BODY})
203
+ }x
204
+
205
+ # wkday = "Mon" | "Tue" | "Wed"
206
+ # | "Thu" | "Fri" | "Sat" | "Sun"
207
+ WKDAY = Regexp.union( %w[Mon Tue Wed Thu Fri Sat Sun] )
208
+
209
+ # month = "Jan" | "Feb" | "Mar" | "Apr"
210
+ # | "May" | "Jun" | "Jul" | "Aug"
211
+ # | "Sep" | "Oct" | "Nov" | "Dec"
212
+ MONTH = Regexp.union( %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec] )
213
+
214
+ # Match an RFC1123 "HTTP date"
215
+ # rfc1123-date = wkday "," SP date1 SP time SP "GMT"
216
+ # date1 = 2DIGIT SP month SP 4DIGIT
217
+ # ; day month year (e.g., 02 Jun 1982)
218
+ # time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
219
+ # ; 00:00:00 - 23:59:59
220
+ HTTP_DATE = %r{
221
+ #{WKDAY} , #{SP}
222
+ \d{2} #{SP}
223
+ #{MONTH} #{SP}
224
+ \d{4} #{SP}
225
+ \d{2} : \d{2} : \d{2} #{SP} GMT
226
+ }x
227
+
228
+
229
+ # Freeze all testing constants
230
+ constants.each do |cname|
231
+ const_get(cname).freeze
232
+ end
233
+ end
234
+
235
+ end
236
+
237
+
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ BEGIN {
5
+ require 'pathname'
6
+ basedir = Pathname.new( __FILE__ ).dirname.parent
7
+
8
+ libdir = basedir + "lib"
9
+
10
+ $LOAD_PATH.unshift( basedir.to_s ) unless $LOAD_PATH.include?( basedir.to_s )
11
+ $LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
12
+ }
13
+
14
+ # SimpleCov test coverage reporting; enable this using the :coverage rake task
15
+ if ENV['COVERAGE']
16
+ $stderr.puts "\n\n>>> Enabling coverage report.\n\n"
17
+ require 'simplecov'
18
+ SimpleCov.start do
19
+ add_filter 'spec'
20
+ add_group "Config Classes" do |file|
21
+ file.filename =~ %r{lib/mongrel2/config(\.rb|/.*)$}
22
+ end
23
+ add_group "Needing tests" do |file|
24
+ file.covered_percent < 90
25
+ end
26
+ end
27
+ end
28
+
29
+ begin
30
+ require 'configurability'
31
+ rescue LoadError => err
32
+ end
33
+
34
+ require 'pathname'
35
+ require 'tmpdir'
36
+
37
+ require 'rspec'
38
+ require 'mongrel2'
39
+ require 'mongrel2/config'
40
+
41
+ require 'sequel'
42
+ require 'sequel/model'
43
+
44
+ require 'spec/lib/constants'
45
+ require 'spec/lib/matchers'
46
+
47
+
48
+ ### RSpec helper functions.
49
+ module Mongrel2::SpecHelpers
50
+ include Mongrel2::TestConstants
51
+
52
+ class ArrayLogger
53
+ ### Create a new ArrayLogger that will append content to +array+.
54
+ def initialize( array )
55
+ @array = array
56
+ end
57
+
58
+ ### Write the specified +message+ to the array.
59
+ def write( message )
60
+ @array << message
61
+ end
62
+
63
+ ### No-op -- this is here just so Logger doesn't complain
64
+ def close; end
65
+
66
+ end # class ArrayLogger
67
+
68
+
69
+ unless defined?( LEVEL )
70
+ LEVEL = {
71
+ :debug => Logger::DEBUG,
72
+ :info => Logger::INFO,
73
+ :warn => Logger::WARN,
74
+ :error => Logger::ERROR,
75
+ :fatal => Logger::FATAL,
76
+ }
77
+ end
78
+
79
+ ###############
80
+ module_function
81
+ ###############
82
+
83
+ ### Make an easily-comparable version vector out of +ver+ and return it.
84
+ def vvec( ver )
85
+ return ver.split('.').collect {|char| char.to_i }.pack('N*')
86
+ end
87
+
88
+
89
+ ### Reset the logging subsystem to its default state.
90
+ def reset_logging
91
+ Mongrel2.reset_logger
92
+ end
93
+
94
+
95
+ ### Alter the output of the default log formatter to be pretty in SpecMate output
96
+ def setup_logging( level=Logger::FATAL )
97
+
98
+ # Turn symbol-style level config into Logger's expected Fixnum level
99
+ if Mongrel2::Logging::LOG_LEVELS.key?( level.to_s )
100
+ level = Mongrel2::Logging::LOG_LEVELS[ level.to_s ]
101
+ end
102
+
103
+ logger = Logger.new( $stderr )
104
+ Mongrel2.logger = logger
105
+ Mongrel2.logger.level = level
106
+
107
+ # Only do this when executing from a spec in TextMate
108
+ if ENV['HTML_LOGGING'] || (ENV['TM_FILENAME'] && ENV['TM_FILENAME'] =~ /_spec\.rb/)
109
+ Thread.current['logger-output'] = []
110
+ logdevice = ArrayLogger.new( Thread.current['logger-output'] )
111
+ Mongrel2.logger = Logger.new( logdevice )
112
+ # Mongrel2.logger.level = level
113
+ Mongrel2.logger.formatter = Mongrel2::Logging::HtmlFormatter.new( logger )
114
+ end
115
+ end
116
+
117
+
118
+ ### Set up a Mongrel2 configuration database in memory.
119
+ def setup_config_db( dbspec=':memory:' )
120
+ Mongrel2::Config.configure( :configdb => dbspec ) unless
121
+ Mongrel2::Config.db.uri[ %r{sqlite:/(.*)}, 1 ] == dbspec
122
+ Mongrel2::Config.init_database
123
+ Mongrel2::Config.db.tables.collect {|t| Mongrel2::Config.db[t] }.each( &:truncate )
124
+ end
125
+
126
+
127
+ ### Normalize and fill in missing members for the given +opts+.
128
+ def normalize_headers( opts, defaults=TEST_HEADERS )
129
+ headers = defaults.merge( opts[:headers] || {} )
130
+
131
+ headers["PATH"] = opts[:path]
132
+ headers["URI"] = "#{opts[:path]}?#{opts[:query]}"
133
+ headers["QUERY"] = opts[:query]
134
+ headers["PATTERN"] = opts[:pattern] || opts[:path]
135
+
136
+ return headers
137
+ end
138
+
139
+
140
+ ### Make a raw Mongrel2 request from the specified +opts+ and return it as a String.
141
+ def make_request( opts={} )
142
+ opts = TEST_REQUEST_OPTS.merge( opts )
143
+ headers = normalize_headers( opts )
144
+
145
+ headerstring = TNetstring.dump( Yajl::Encoder.encode(headers) )
146
+ bodystring = TNetstring.dump( opts[:body] || '' )
147
+
148
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
149
+ return "%s %d %s %s%s" % [
150
+ opts[:uuid],
151
+ opts[:id],
152
+ opts[:path],
153
+ headerstring,
154
+ bodystring,
155
+ ]
156
+ end
157
+
158
+
159
+ ### Make a new-style (TNetstring headers) raw Mongrel2 request from the specified +opts+
160
+ ### and return it as a String.
161
+ def make_tn_request( opts={} )
162
+ opts = TEST_REQUEST_OPTS.merge( opts )
163
+ headers = normalize_headers( opts )
164
+
165
+ headerstring = TNetstring.dump( headers )
166
+ bodystring = TNetstring.dump( opts[:body] || '' )
167
+
168
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
169
+ return "%s %d %s %s%s" % [
170
+ opts[:uuid],
171
+ opts[:id],
172
+ opts[:path],
173
+ headerstring,
174
+ bodystring,
175
+ ]
176
+ end
177
+
178
+
179
+ ### Make a Mongrel2 request for a JSON route.
180
+ def make_json_request( opts={} )
181
+ opts = TEST_JSON_REQUEST_OPTS.merge( opts )
182
+ headers = normalize_headers( opts, TEST_JSON_HEADERS )
183
+ headers.delete( 'URI' ) # JSON requests don't have one
184
+
185
+ Mongrel2.log.debug "JSON request, headers = %p, opts = %p" % [ headers, opts ]
186
+
187
+ headerstring = TNetstring.dump( Yajl::Encoder.encode(headers) )
188
+ bodystring = TNetstring.dump( Yajl::Encoder.encode(opts[:body] || []) )
189
+
190
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
191
+ return "%s %d %s %s%s" % [
192
+ opts[:uuid],
193
+ opts[:id],
194
+ opts[:path],
195
+ headerstring,
196
+ bodystring,
197
+ ]
198
+ end
199
+
200
+
201
+ ### Make a Mongrel2 request for an XML route.
202
+ def make_xml_request( opts={} )
203
+ opts = TEST_XML_REQUEST_OPTS.merge( opts )
204
+ headers = normalize_headers( opts, TEST_XML_HEADERS )
205
+ headers.delete( 'URI' ) # XML requests don't have one
206
+
207
+ Mongrel2.log.debug "XML request, headers = %p, opts = %p" % [ headers, opts ]
208
+
209
+ headerstring = TNetstring.dump( Yajl::Encoder.encode(headers) )
210
+ bodystring = TNetstring.dump( opts[:body] || "#{TEST_XML_PATH} />" )
211
+
212
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
213
+ return "%s %d %s %s%s" % [
214
+ opts[:uuid],
215
+ opts[:id],
216
+ opts[:path],
217
+ headerstring,
218
+ bodystring,
219
+ ]
220
+ end
221
+
222
+ end
223
+
224
+
225
+ abort "You need a version of RSpec >= 2.6.0" unless defined?( RSpec )
226
+
227
+ if defined?( ::Amalgalite )
228
+ $stderr.puts ">>> Using Amalgalite #{Amalgalite::VERSION} for DB access."
229
+ else
230
+ $stderr.puts ">>> Using SQLite3 #{SQLite3::VERSION} for DB access."
231
+ end
232
+
233
+ ### Mock with RSpec
234
+ RSpec.configure do |c|
235
+ include Mongrel2::TestConstants
236
+
237
+ c.mock_with :rspec
238
+
239
+ c.extend( Mongrel2::TestConstants )
240
+ c.include( Mongrel2::TestConstants )
241
+ c.include( Mongrel2::SpecHelpers )
242
+ c.include( Mongrel2::Matchers )
243
+ end
244
+
245
+ # vim: set nosta noet ts=4 sw=4:
246
+