wire-framework 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +340 -0
- data/README.md +6 -0
- data/lib/app.rb +27 -0
- data/lib/app/db.rb +174 -0
- data/lib/app/file.rb +81 -0
- data/lib/app/history.rb +59 -0
- data/lib/app/history/svn.rb +34 -0
- data/lib/app/login.rb +14 -0
- data/lib/app/render.rb +121 -0
- data/lib/app/render/document.rb +51 -0
- data/lib/app/render/editor.rb +55 -0
- data/lib/app/render/instant.rb +65 -0
- data/lib/app/render/page.rb +91 -0
- data/lib/app/render/partial.rb +110 -0
- data/lib/app/render/style.rb +52 -0
- data/lib/app/repo.rb +148 -0
- data/lib/app/repo/svn.rb +177 -0
- data/lib/closet.rb +99 -0
- data/lib/closet/auth.rb +55 -0
- data/lib/closet/context.rb +103 -0
- data/lib/closet/renderer.rb +46 -0
- data/lib/closet/resource.rb +18 -0
- data/lib/wire.rb +28 -0
- metadata +208 -0
data/lib/app/file.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'awesome_print'
|
|
2
|
+
require_relative '../wire'
|
|
3
|
+
|
|
4
|
+
# Static is a Wire::App for serving read-only, static files
|
|
5
|
+
# @author Bryan T. Meyers
|
|
6
|
+
module Static
|
|
7
|
+
extend Wire::App
|
|
8
|
+
extend Wire::Resource
|
|
9
|
+
|
|
10
|
+
# Map a file folder to a sub-URI
|
|
11
|
+
# @param [String] resource the sub-URI
|
|
12
|
+
# @param [Class] path the file folder
|
|
13
|
+
# @return [void]
|
|
14
|
+
def self.local(resource, path)
|
|
15
|
+
$current_app[:resources][resource] = { local: path }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get a file listing for this folder
|
|
19
|
+
# @param [Hash] context the context for this request
|
|
20
|
+
# @return [Response] a listing, or status code
|
|
21
|
+
def self.do_read_all(context)
|
|
22
|
+
path = context.resource[:local]
|
|
23
|
+
if path
|
|
24
|
+
return 404 unless File.exists?(path)
|
|
25
|
+
if File.directory? path
|
|
26
|
+
Dir.entries(path).sort.to_s
|
|
27
|
+
else
|
|
28
|
+
401
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
404
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get a file from this folder
|
|
36
|
+
# @param [Hash] context the context for this request
|
|
37
|
+
# @return [Response] a file, listing, or status code
|
|
38
|
+
def self.do_read(context)
|
|
39
|
+
path = context.resource[:local]
|
|
40
|
+
id = context.uri[3..context.uri.length].join('/')
|
|
41
|
+
if path
|
|
42
|
+
ext_path = File.join(path, id)
|
|
43
|
+
return 404 unless File.exists?(ext_path)
|
|
44
|
+
if File.directory?(ext_path)
|
|
45
|
+
"#{ap Dir.entries(ext_path).sort}"
|
|
46
|
+
else
|
|
47
|
+
if ext_path.end_with?('.wiki') || ext_path.end_with?('.mediawiki')
|
|
48
|
+
mime = 'text/wiki'
|
|
49
|
+
else
|
|
50
|
+
mime = `mimetype --brief #{ext_path}`
|
|
51
|
+
end
|
|
52
|
+
headers = {}
|
|
53
|
+
headers['Content-Type'] = mime
|
|
54
|
+
headers['Cache-Control'] = 'public'
|
|
55
|
+
headers['Expires'] = "#{(Time.now + 30000000).utc}"
|
|
56
|
+
body = File.read(ext_path)
|
|
57
|
+
[200, headers, body]
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
404
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Proxy method used when routing
|
|
65
|
+
# @param [Array] actions the allowed actions for this URI
|
|
66
|
+
# @param [Hash] context the context for this request
|
|
67
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
68
|
+
def self.invoke(actions, context)
|
|
69
|
+
return 404 unless context.resource
|
|
70
|
+
case context.action
|
|
71
|
+
when :read
|
|
72
|
+
if context.uri[3]
|
|
73
|
+
do_read(context)
|
|
74
|
+
else
|
|
75
|
+
do_read_all(context)
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
403
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/app/history.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'awesome_print'
|
|
2
|
+
require 'tilt'
|
|
3
|
+
require_relative '../wire'
|
|
4
|
+
require_relative 'history/svn'
|
|
5
|
+
|
|
6
|
+
# History is a Wire::App for accessing the history of versioned content
|
|
7
|
+
# @author Bryan T. Meyers
|
|
8
|
+
module History
|
|
9
|
+
|
|
10
|
+
# Select the location of the repositories
|
|
11
|
+
# @param [Symbol] path location of the repositories
|
|
12
|
+
# @return [void]
|
|
13
|
+
def repos(path)
|
|
14
|
+
$current_app[:repos_path] = path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Select the render template for histories
|
|
18
|
+
# @param [Symbol] path location of the Tilt compatible template
|
|
19
|
+
# @return [void]
|
|
20
|
+
def log(path)
|
|
21
|
+
$current_app[:template] = Tilt.new(path, 1, { ugly: true })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Select the sub-directory for web-serveable content
|
|
25
|
+
# @param [Symbol] path the sub-directory path
|
|
26
|
+
# @return [void]
|
|
27
|
+
def web_folder(path)
|
|
28
|
+
$current_app[:web] = path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get the history of a single file or directory
|
|
32
|
+
# @param [Hash] context the context for this request
|
|
33
|
+
# @return [Response] the history, or status code
|
|
34
|
+
def do_read(actions, context)
|
|
35
|
+
resource = context.uri[2]
|
|
36
|
+
web = context.app[:web]
|
|
37
|
+
id = context.uri[3...context.uri.length].join('/')
|
|
38
|
+
list = get_log(web, resource, id)
|
|
39
|
+
if list == 404
|
|
40
|
+
return 404
|
|
41
|
+
end
|
|
42
|
+
template = context.app[:template]
|
|
43
|
+
template.render(self, actions: actions, context: context, list: list)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Proxy method used when routing
|
|
47
|
+
# @param [Array] actions the allowed actions for this URI
|
|
48
|
+
# @param [Hash] context the context for this request
|
|
49
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
50
|
+
def invoke(actions, context)
|
|
51
|
+
return 404 unless context.uri[2]
|
|
52
|
+
case context.action
|
|
53
|
+
when :read
|
|
54
|
+
do_read(actions, context)
|
|
55
|
+
else
|
|
56
|
+
403
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative '../repo'
|
|
2
|
+
require 'cobravsmongoose'
|
|
3
|
+
|
|
4
|
+
module History
|
|
5
|
+
# History::SVN is a connector for viewing log information in SVN
|
|
6
|
+
# @author Bryan T. Meyers
|
|
7
|
+
module SVN
|
|
8
|
+
extend Wire::App
|
|
9
|
+
extend Wire::Resource
|
|
10
|
+
extend History
|
|
11
|
+
|
|
12
|
+
# Get the log information for any part of a Repo
|
|
13
|
+
# @param [String] web the web path of the repo
|
|
14
|
+
# @param [String] repo the name of the repository to access
|
|
15
|
+
# @param [String] id the sub-URI of the item to access
|
|
16
|
+
# @return [Hash] the history entries
|
|
17
|
+
def self.get_log(web, repo, id = nil)
|
|
18
|
+
if id.nil?
|
|
19
|
+
log = `svn log -v --xml 'svn://localhost/#{repo}'`
|
|
20
|
+
else
|
|
21
|
+
if web.nil?
|
|
22
|
+
log = `svn log -v --xml 'svn://localhost/#{repo}/#{id}'`
|
|
23
|
+
else
|
|
24
|
+
log = `svn log -v --xml 'svn://localhost/#{repo}/#{web}/#{id}'`
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
unless $?.exitstatus == 0
|
|
28
|
+
return 404
|
|
29
|
+
end
|
|
30
|
+
log = CobraVsMongoose.xml_to_hash(log)
|
|
31
|
+
log['log']['logentry']
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/app/login.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Login is a Wire::App for forcing user logins
|
|
2
|
+
# @author Bryan T. Meyers
|
|
3
|
+
module Login
|
|
4
|
+
|
|
5
|
+
# Proxy method used when routing
|
|
6
|
+
# @param [Array] actions the allowed actions for this URI
|
|
7
|
+
# @param [Hash] context the context for this request
|
|
8
|
+
# @return [Response] a redirect message returning to the previous page
|
|
9
|
+
def self.invoke(actions, context)
|
|
10
|
+
referer = context.referer
|
|
11
|
+
[301, { 'Location' => referer }, ['Login Redirect']]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
end
|
data/lib/app/render.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'rest-client'
|
|
3
|
+
require 'awesome_print'
|
|
4
|
+
require 'docile'
|
|
5
|
+
require 'tilt'
|
|
6
|
+
require 'json'
|
|
7
|
+
require_relative '../wire'
|
|
8
|
+
require_relative 'render/document'
|
|
9
|
+
require_relative 'render/editor'
|
|
10
|
+
require_relative 'render/instant'
|
|
11
|
+
require_relative 'render/page'
|
|
12
|
+
require_relative 'render/partial'
|
|
13
|
+
require_relative 'render/style'
|
|
14
|
+
|
|
15
|
+
# Render is an Abstract Wire::App for transforming content
|
|
16
|
+
# @author Bryan T. Meyers
|
|
17
|
+
module Render
|
|
18
|
+
include Wire::Resource
|
|
19
|
+
|
|
20
|
+
# Setup the remote connection for content
|
|
21
|
+
# @param [String] hostname http hostname and port of remote
|
|
22
|
+
# @param [String] uri the base URI for content on the remote
|
|
23
|
+
# @return [void]
|
|
24
|
+
def remote(hostname, uri)
|
|
25
|
+
$current_app[:remote_host] = hostname
|
|
26
|
+
$current_app[:remote_uri] = uri
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Setup the template for rendering
|
|
30
|
+
# @param [String] path location of the template
|
|
31
|
+
# @param [Proc] block block to execute for setting up template
|
|
32
|
+
# @return [void]
|
|
33
|
+
def template(path, &block)
|
|
34
|
+
$current_app[:template] = { path: path.nil? ? nil : Tilt.new(path, 1, { ugly: true }), sources: {} }
|
|
35
|
+
if block
|
|
36
|
+
Docile.dsl_eval(self, &block)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Enable the use of an outer layout, for the current template
|
|
41
|
+
def use_layout
|
|
42
|
+
$current_app[:template][:use_layout] = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Setup the source for additional template information
|
|
46
|
+
# @param [Symbol] key the type of key for this source
|
|
47
|
+
# @param [String] uri the path for a remote source
|
|
48
|
+
# @param [Proc] block block to execute for setting up source
|
|
49
|
+
# @return [void]
|
|
50
|
+
def source(key, uri, &block)
|
|
51
|
+
$current_app[:template][:sources][key] = { uri: uri, key: nil }
|
|
52
|
+
$current_source = $current_app[:template][:sources][key]
|
|
53
|
+
if block
|
|
54
|
+
Docile.dsl_eval(self, &block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Setup the key for additional source information
|
|
59
|
+
# @param [Symbol] type the type of key for this source
|
|
60
|
+
# @return [void]
|
|
61
|
+
def key(type)
|
|
62
|
+
$current_source[:key] = type
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Setup the template for rendering single items
|
|
66
|
+
# @param [String] template the path to the template
|
|
67
|
+
# @return [void]
|
|
68
|
+
def single(template)
|
|
69
|
+
partial = Tilt.new(template, 1, { ugly: true })
|
|
70
|
+
$current_resource[:single] = partial
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Setup the template for rendering multiple items
|
|
74
|
+
# @param [String] template the path to the template
|
|
75
|
+
# @return [void]
|
|
76
|
+
def multiple(template)
|
|
77
|
+
partial = Tilt.new(template, 1, { ugly: true })
|
|
78
|
+
$current_resource[:multiple] = partial
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Setup the template for rendering one or more items
|
|
82
|
+
# @param [String] template the path to the template
|
|
83
|
+
# @return [void]
|
|
84
|
+
def all(template)
|
|
85
|
+
multiple(template)
|
|
86
|
+
single(template)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Proxy method used when forwarding requests
|
|
90
|
+
# @param [Symbol] method the action to use when forwarding
|
|
91
|
+
# @param [Hash] context the context for this request
|
|
92
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
93
|
+
def forward(method, context)
|
|
94
|
+
host = context.app[:remote_host]
|
|
95
|
+
path = context.app[:remote_uri]
|
|
96
|
+
resource = context.uri[2]
|
|
97
|
+
referer = context.referer.join('/')
|
|
98
|
+
|
|
99
|
+
q = '?' + context.query_string
|
|
100
|
+
id = context.uri[3...context.uri.length].join('/')
|
|
101
|
+
case (method)
|
|
102
|
+
when :create
|
|
103
|
+
puts "POST: Forward Request to https://#{host}/#{path}/#{resource}#{q}"
|
|
104
|
+
RestClient.post "http://#{host}/#{path}/#{resource}#{q}", context.body
|
|
105
|
+
when :update
|
|
106
|
+
puts "PUT: Forward Request to https://#{host}/#{path}/#{resource}/#{id}#{q}"
|
|
107
|
+
RestClient.put "http://#{host}/#{path}/#{resource}/#{id}#{q}", context.body
|
|
108
|
+
when :readAll
|
|
109
|
+
puts "GET: Forward Request to https://#{host}/#{path}/#{resource}#{q}"
|
|
110
|
+
RestClient.get "http://#{host}/#{path}/#{resource}#{q}", referer: referer
|
|
111
|
+
when :read
|
|
112
|
+
puts "GET: Forward Request to https://#{host}/#{path}/#{resource}/#{id}#{q}"
|
|
113
|
+
RestClient.get "http://#{host}/#{path}/#{resource}/#{id}#{q}", referer: referer
|
|
114
|
+
when :delete
|
|
115
|
+
puts "DELETE: Forward Request to https://#{host}/#{path}/#{resource}/#{id}#{q}"
|
|
116
|
+
RestClient.delete "http://#{host}/#{path}/#{resource}/#{id}#{q}"
|
|
117
|
+
else
|
|
118
|
+
401
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require_relative '../render'
|
|
2
|
+
|
|
3
|
+
module Render
|
|
4
|
+
# Document renders a file to an HTML representation
|
|
5
|
+
# @author Bryan T. Meyers
|
|
6
|
+
module Document
|
|
7
|
+
extend Render
|
|
8
|
+
|
|
9
|
+
# Renders a document or listing to HTML
|
|
10
|
+
# @param [Array] actions the actions allowed for this URI
|
|
11
|
+
# @param [Wire::Context] context the context for this request
|
|
12
|
+
# @param [Symbol] specific the type of read to perform
|
|
13
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
14
|
+
def self.do_read(actions, context, specific)
|
|
15
|
+
begin
|
|
16
|
+
response = forward(specific, context)
|
|
17
|
+
mime = response.headers[:content_type]
|
|
18
|
+
renderer = $renderers[mime]
|
|
19
|
+
if renderer
|
|
20
|
+
template = $templates[renderer]
|
|
21
|
+
template.render(self, { actions: actions, context: context, mime: mime, response: response.body })
|
|
22
|
+
else
|
|
23
|
+
response
|
|
24
|
+
end
|
|
25
|
+
rescue RestClient::ResourceNotFound
|
|
26
|
+
[404, {}, $apps[404][:template][:path].render(self, locals = { actions: actions, context: context })]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Proxy method used when routing
|
|
31
|
+
# @param [Array] actions the allowed actions for this URI
|
|
32
|
+
# @param [Hash] context the context for this request
|
|
33
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
34
|
+
def self.invoke(actions, context)
|
|
35
|
+
case context.action
|
|
36
|
+
when :create
|
|
37
|
+
forward(:create, context)
|
|
38
|
+
when :read
|
|
39
|
+
if context.uri[3]
|
|
40
|
+
do_read(actions, context, :read)
|
|
41
|
+
else
|
|
42
|
+
do_read(actions, context, :readAll)
|
|
43
|
+
end
|
|
44
|
+
when :update
|
|
45
|
+
forward(:update, context)
|
|
46
|
+
else
|
|
47
|
+
405
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative '../render'
|
|
2
|
+
|
|
3
|
+
module Render
|
|
4
|
+
|
|
5
|
+
# Editor allows a document to be displayed in an editing form
|
|
6
|
+
# @author Bryan T. Meyers
|
|
7
|
+
module Editor
|
|
8
|
+
extend Render
|
|
9
|
+
|
|
10
|
+
# Open an editor for a document
|
|
11
|
+
# @param [Array] actions the allowed actions for this URI
|
|
12
|
+
# @param [Hash] context the context for this request
|
|
13
|
+
# @return [Response] the Editor with containing document, or status code
|
|
14
|
+
def self.do_read(actions, context)
|
|
15
|
+
resource = context.uri[2]
|
|
16
|
+
query = context.query
|
|
17
|
+
id = context.uri[3...context.uri.length].join('/')
|
|
18
|
+
body = ''
|
|
19
|
+
begin
|
|
20
|
+
response = forward(:read, context)
|
|
21
|
+
mime = response.headers[:content_type]
|
|
22
|
+
body = response.body
|
|
23
|
+
rescue RestClient::ResourceNotFound
|
|
24
|
+
if query[:type]
|
|
25
|
+
mime = query[:type]
|
|
26
|
+
else
|
|
27
|
+
return [404, {}, 'EDITOR: Document type not set for new document']
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
template = $editors[mime]
|
|
31
|
+
if template
|
|
32
|
+
template.render(self, { actions: actions, resource: resource, id: id, mime: mime, response: body })
|
|
33
|
+
else
|
|
34
|
+
body
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Proxy method used when routing
|
|
39
|
+
# @param [Array] actions the allowed actions for this URI
|
|
40
|
+
# @param [Hash] context the context for this request
|
|
41
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
42
|
+
def self.invoke(actions, context)
|
|
43
|
+
case context.action
|
|
44
|
+
when :create
|
|
45
|
+
forward(:create, context)
|
|
46
|
+
when :read
|
|
47
|
+
do_read(actions, context)
|
|
48
|
+
when :update
|
|
49
|
+
forward(:update, context)
|
|
50
|
+
else
|
|
51
|
+
405
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require_relative '../render'
|
|
2
|
+
|
|
3
|
+
module Render
|
|
4
|
+
# Instant allows for previews of edited documents
|
|
5
|
+
# @author Bryan T. Meyers
|
|
6
|
+
module Instant
|
|
7
|
+
extend Render
|
|
8
|
+
|
|
9
|
+
# Render a temporary document to HTML
|
|
10
|
+
# @param [Array] actions the allowed actions for this URI
|
|
11
|
+
# @param [Hash] context the context for this request
|
|
12
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
13
|
+
def self.do_update(actions, context)
|
|
14
|
+
body = context.body
|
|
15
|
+
if body
|
|
16
|
+
body = body.split('=')[1]
|
|
17
|
+
if body
|
|
18
|
+
body = URI.decode(body)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
resource = context.uri[2]
|
|
22
|
+
id = context.uri[3]
|
|
23
|
+
## Default to not found
|
|
24
|
+
message = ''
|
|
25
|
+
status = 404
|
|
26
|
+
if resource
|
|
27
|
+
if body
|
|
28
|
+
## Assume unsupported mime type
|
|
29
|
+
status = 415
|
|
30
|
+
message = 'INSTANT: Unsupported MIME Type'
|
|
31
|
+
renderer = $renderers["#{resource}/#{id}"]
|
|
32
|
+
if renderer
|
|
33
|
+
template = $templates[renderer]
|
|
34
|
+
result = template.render(self,
|
|
35
|
+
{ actions: actions,
|
|
36
|
+
context: context,
|
|
37
|
+
mime: "#{resource}/#{id}",
|
|
38
|
+
response: body,
|
|
39
|
+
})
|
|
40
|
+
template = context.app[:template]
|
|
41
|
+
if template
|
|
42
|
+
message = template[:path].render(self, { actions: actions, context: context, content: result })
|
|
43
|
+
else
|
|
44
|
+
message = result
|
|
45
|
+
end
|
|
46
|
+
status = 200
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
[status, {}, message]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Proxy method used when routing
|
|
54
|
+
# @param [Array] actions the allowed actions for this URI
|
|
55
|
+
# @param [Hash] context the context for this request
|
|
56
|
+
# @return [Response] a Rack Response triplet, or status code
|
|
57
|
+
def self.invoke(actions, context)
|
|
58
|
+
if context.action.eql? :update
|
|
59
|
+
do_update(actions, context)
|
|
60
|
+
else
|
|
61
|
+
405
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|