arrow 1.0.7

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 (198) hide show
  1. data/ChangeLog +1590 -0
  2. data/LICENSE +28 -0
  3. data/README +75 -0
  4. data/Rakefile +366 -0
  5. data/Rakefile.local +63 -0
  6. data/data/arrow/applets/TEMPLATE.rb.tpl +53 -0
  7. data/data/arrow/applets/args.rb +50 -0
  8. data/data/arrow/applets/config.rb +55 -0
  9. data/data/arrow/applets/error.rb +63 -0
  10. data/data/arrow/applets/files.rb +46 -0
  11. data/data/arrow/applets/inspect.rb +46 -0
  12. data/data/arrow/applets/nosuchapplet.rb +31 -0
  13. data/data/arrow/applets/status.rb +92 -0
  14. data/data/arrow/applets/test.rb +133 -0
  15. data/data/arrow/applets/tutorial/counter.rb +96 -0
  16. data/data/arrow/applets/tutorial/dingus.rb +67 -0
  17. data/data/arrow/applets/tutorial/hello.rb +34 -0
  18. data/data/arrow/applets/tutorial/hello2.rb +73 -0
  19. data/data/arrow/applets/tutorial/imgtext.rb +90 -0
  20. data/data/arrow/applets/tutorial/imgtext2.rb +286 -0
  21. data/data/arrow/applets/tutorial/index.rb +36 -0
  22. data/data/arrow/applets/tutorial/logo.rb +98 -0
  23. data/data/arrow/applets/tutorial/memcache.rb +61 -0
  24. data/data/arrow/applets/tutorial/missing.rb +37 -0
  25. data/data/arrow/applets/tutorial/protected.rb +100 -0
  26. data/data/arrow/applets/tutorial/redirector.rb +52 -0
  27. data/data/arrow/applets/tutorial/rndimages.rb +159 -0
  28. data/data/arrow/applets/tutorial/sharenotes.rb +83 -0
  29. data/data/arrow/applets/tutorial/subclassed-hello.rb +32 -0
  30. data/data/arrow/applets/tutorial/superhello.rb +72 -0
  31. data/data/arrow/applets/tutorial/timeclock.rb +78 -0
  32. data/data/arrow/applets/view-applet.rb +123 -0
  33. data/data/arrow/applets/view-template.rb +85 -0
  34. data/data/arrow/applets/wiki.rb +274 -0
  35. data/data/arrow/templates/TEMPLATE.tmpl.tpl +36 -0
  36. data/data/arrow/templates/applet-status.tmpl +153 -0
  37. data/data/arrow/templates/args-display.tmpl +120 -0
  38. data/data/arrow/templates/config/display-table.tmpl +36 -0
  39. data/data/arrow/templates/config/display.tmpl +36 -0
  40. data/data/arrow/templates/counter-deleted.tmpl +33 -0
  41. data/data/arrow/templates/counter.tmpl +59 -0
  42. data/data/arrow/templates/dingus.tmpl +55 -0
  43. data/data/arrow/templates/enumtable.tmpl +8 -0
  44. data/data/arrow/templates/error-display.tmpl +92 -0
  45. data/data/arrow/templates/filemap.tmpl +89 -0
  46. data/data/arrow/templates/hello-world-src.tmpl +34 -0
  47. data/data/arrow/templates/hello-world.tmpl +60 -0
  48. data/data/arrow/templates/imgtext/fontlist.tmpl +46 -0
  49. data/data/arrow/templates/imgtext/form.tmpl +70 -0
  50. data/data/arrow/templates/imgtext/reload-error.tmpl +40 -0
  51. data/data/arrow/templates/imgtext/reload.tmpl +55 -0
  52. data/data/arrow/templates/inspect/display.tmpl +80 -0
  53. data/data/arrow/templates/loginform.tmpl +64 -0
  54. data/data/arrow/templates/logout.tmpl +32 -0
  55. data/data/arrow/templates/memcache/display.tmpl +41 -0
  56. data/data/arrow/templates/navbar.incl +27 -0
  57. data/data/arrow/templates/nosuchapplet.tmpl +32 -0
  58. data/data/arrow/templates/printsource.tmpl +35 -0
  59. data/data/arrow/templates/protected.tmpl +36 -0
  60. data/data/arrow/templates/rndimages.tmpl +38 -0
  61. data/data/arrow/templates/service-response.tmpl +13 -0
  62. data/data/arrow/templates/sharenotes/display.tmpl +38 -0
  63. data/data/arrow/templates/status.tmpl +120 -0
  64. data/data/arrow/templates/templateviewer.tmpl +43 -0
  65. data/data/arrow/templates/test/harness.tmpl +57 -0
  66. data/data/arrow/templates/test/list.tmpl +48 -0
  67. data/data/arrow/templates/test/problem.tmpl +42 -0
  68. data/data/arrow/templates/tutorial/index.tmpl +37 -0
  69. data/data/arrow/templates/tutorial/missingapplet.tmpl +29 -0
  70. data/data/arrow/templates/view-applet-nosuch.tmpl +32 -0
  71. data/data/arrow/templates/view-applet.tmpl +40 -0
  72. data/data/arrow/templates/view-template.tmpl +83 -0
  73. data/data/arrow/templates/wiki/formerror.tmpl +47 -0
  74. data/data/arrow/templates/wiki/markup_help.incl +6 -0
  75. data/data/arrow/templates/wiki/new.tmpl +56 -0
  76. data/data/arrow/templates/wiki/new_system.tmpl +122 -0
  77. data/data/arrow/templates/wiki/sectionlist.tmpl +43 -0
  78. data/data/arrow/templates/wiki/show.tmpl +34 -0
  79. data/docs/manual/layouts/default.page +43 -0
  80. data/docs/manual/lib/api-filter.rb +81 -0
  81. data/docs/manual/lib/editorial-filter.rb +64 -0
  82. data/docs/manual/lib/examples-filter.rb +244 -0
  83. data/docs/manual/lib/links-filter.rb +117 -0
  84. data/lib/apache/fakerequest.rb +448 -0
  85. data/lib/apache/logger.rb +33 -0
  86. data/lib/arrow.rb +51 -0
  87. data/lib/arrow/acceptparam.rb +207 -0
  88. data/lib/arrow/applet.rb +725 -0
  89. data/lib/arrow/appletmixins.rb +218 -0
  90. data/lib/arrow/appletregistry.rb +590 -0
  91. data/lib/arrow/applettestcase.rb +503 -0
  92. data/lib/arrow/broker.rb +255 -0
  93. data/lib/arrow/cache.rb +176 -0
  94. data/lib/arrow/config-loaders/yaml.rb +75 -0
  95. data/lib/arrow/config.rb +615 -0
  96. data/lib/arrow/constants.rb +24 -0
  97. data/lib/arrow/cookie.rb +359 -0
  98. data/lib/arrow/cookieset.rb +108 -0
  99. data/lib/arrow/dispatcher.rb +368 -0
  100. data/lib/arrow/dispatcherloader.rb +50 -0
  101. data/lib/arrow/exceptions.rb +61 -0
  102. data/lib/arrow/fallbackhandler.rb +48 -0
  103. data/lib/arrow/formvalidator.rb +631 -0
  104. data/lib/arrow/htmltokenizer.rb +343 -0
  105. data/lib/arrow/logger.rb +488 -0
  106. data/lib/arrow/logger/apacheoutputter.rb +69 -0
  107. data/lib/arrow/logger/arrayoutputter.rb +63 -0
  108. data/lib/arrow/logger/coloroutputter.rb +111 -0
  109. data/lib/arrow/logger/fileoutputter.rb +96 -0
  110. data/lib/arrow/logger/htmloutputter.rb +54 -0
  111. data/lib/arrow/logger/outputter.rb +123 -0
  112. data/lib/arrow/mixins.rb +425 -0
  113. data/lib/arrow/monkeypatches.rb +94 -0
  114. data/lib/arrow/object.rb +117 -0
  115. data/lib/arrow/path.rb +196 -0
  116. data/lib/arrow/service.rb +447 -0
  117. data/lib/arrow/session.rb +289 -0
  118. data/lib/arrow/session/dbstore.rb +100 -0
  119. data/lib/arrow/session/filelock.rb +160 -0
  120. data/lib/arrow/session/filestore.rb +132 -0
  121. data/lib/arrow/session/id.rb +98 -0
  122. data/lib/arrow/session/lock.rb +253 -0
  123. data/lib/arrow/session/md5id.rb +42 -0
  124. data/lib/arrow/session/nulllock.rb +42 -0
  125. data/lib/arrow/session/posixlock.rb +166 -0
  126. data/lib/arrow/session/sha1id.rb +54 -0
  127. data/lib/arrow/session/store.rb +366 -0
  128. data/lib/arrow/session/usertrackid.rb +52 -0
  129. data/lib/arrow/spechelpers.rb +73 -0
  130. data/lib/arrow/template.rb +713 -0
  131. data/lib/arrow/template/attr.rb +31 -0
  132. data/lib/arrow/template/call.rb +31 -0
  133. data/lib/arrow/template/comment.rb +33 -0
  134. data/lib/arrow/template/container.rb +118 -0
  135. data/lib/arrow/template/else.rb +41 -0
  136. data/lib/arrow/template/elsif.rb +44 -0
  137. data/lib/arrow/template/escape.rb +53 -0
  138. data/lib/arrow/template/export.rb +87 -0
  139. data/lib/arrow/template/for.rb +145 -0
  140. data/lib/arrow/template/if.rb +78 -0
  141. data/lib/arrow/template/import.rb +119 -0
  142. data/lib/arrow/template/include.rb +206 -0
  143. data/lib/arrow/template/iterator.rb +208 -0
  144. data/lib/arrow/template/nodes.rb +734 -0
  145. data/lib/arrow/template/parser.rb +571 -0
  146. data/lib/arrow/template/prettyprint.rb +53 -0
  147. data/lib/arrow/template/render.rb +191 -0
  148. data/lib/arrow/template/selectlist.rb +94 -0
  149. data/lib/arrow/template/set.rb +87 -0
  150. data/lib/arrow/template/timedelta.rb +81 -0
  151. data/lib/arrow/template/unless.rb +78 -0
  152. data/lib/arrow/template/urlencode.rb +51 -0
  153. data/lib/arrow/template/yield.rb +139 -0
  154. data/lib/arrow/templatefactory.rb +125 -0
  155. data/lib/arrow/testcase.rb +567 -0
  156. data/lib/arrow/transaction.rb +608 -0
  157. data/rake/191_compat.rb +26 -0
  158. data/rake/dependencies.rb +76 -0
  159. data/rake/documentation.rb +114 -0
  160. data/rake/helpers.rb +502 -0
  161. data/rake/hg.rb +282 -0
  162. data/rake/manual.rb +787 -0
  163. data/rake/packaging.rb +129 -0
  164. data/rake/publishing.rb +278 -0
  165. data/rake/style.rb +62 -0
  166. data/rake/svn.rb +668 -0
  167. data/rake/testing.rb +187 -0
  168. data/rake/verifytask.rb +64 -0
  169. data/spec/arrow/acceptparam_spec.rb +157 -0
  170. data/spec/arrow/applet_spec.rb +575 -0
  171. data/spec/arrow/appletmixins_spec.rb +409 -0
  172. data/spec/arrow/appletregistry_spec.rb +294 -0
  173. data/spec/arrow/broker_spec.rb +153 -0
  174. data/spec/arrow/config_spec.rb +224 -0
  175. data/spec/arrow/cookieset_spec.rb +164 -0
  176. data/spec/arrow/dispatcher_spec.rb +137 -0
  177. data/spec/arrow/dispatcherloader_spec.rb +65 -0
  178. data/spec/arrow/formvalidator_spec.rb +781 -0
  179. data/spec/arrow/logger_spec.rb +346 -0
  180. data/spec/arrow/mixins_spec.rb +120 -0
  181. data/spec/arrow/service_spec.rb +645 -0
  182. data/spec/arrow/session_spec.rb +121 -0
  183. data/spec/arrow/template/iterator_spec.rb +222 -0
  184. data/spec/arrow/templatefactory_spec.rb +185 -0
  185. data/spec/arrow/transaction_spec.rb +319 -0
  186. data/spec/arrow_spec.rb +37 -0
  187. data/spec/lib/appletmatchers.rb +281 -0
  188. data/spec/lib/constants.rb +77 -0
  189. data/spec/lib/helpers.rb +41 -0
  190. data/spec/lib/matchers.rb +44 -0
  191. data/tests/cookie.tests.rb +310 -0
  192. data/tests/path.tests.rb +157 -0
  193. data/tests/session.tests.rb +111 -0
  194. data/tests/session_id.tests.rb +82 -0
  195. data/tests/session_lock.tests.rb +191 -0
  196. data/tests/session_store.tests.rb +53 -0
  197. data/tests/template.tests.rb +1360 -0
  198. metadata +339 -0
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logger'
4
+
5
+ module Apache
6
+
7
+ # An adapter object that can be used as a log device for a Logger instance that
8
+ # sends log messages to Apache's logging subsystem at 'debug' level.
9
+ class LogDevice < Logger::LogDevice
10
+
11
+ def initialize( *args ); end
12
+
13
+ ### Write a logging message to Apache's debug log.
14
+ def write( message )
15
+ Apache.request.server.log_debug( message )
16
+ end
17
+
18
+ ### No-op -- this is here just so Logger doesn't complain
19
+ def close; end
20
+
21
+ end # class LogDevice
22
+
23
+ # A formatter for log messages that will be forwarded into Apache's log system.
24
+ class LogFormatter < Logger::Formatter
25
+
26
+ def call( severity, time, progname, msg )
27
+ return "[%s] %s: %s" % [ severity, progname, msg ]
28
+ end
29
+
30
+ end
31
+
32
+ end # module Apache
33
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'tmpdir'
5
+
6
+ require 'pathname'
7
+
8
+
9
+ #
10
+ # The Arrow module, a namespace container for classes in the
11
+ # Arrow web application framework.
12
+ #
13
+ # == Authors
14
+ #
15
+ # * Martin Chase <mchase@rubycrafters.com>
16
+ # * Michael Granger <mgranger@rubycrafters.com>
17
+ # * David McCorkhill <dmccorkhill@rubycrafters.com>
18
+ #
19
+ # Please see the file LICENSE in the top-level directory for licensing details.
20
+ #
21
+ module Arrow
22
+
23
+ # Library version
24
+ VERSION = '1.0.7'
25
+
26
+ # VCS revision
27
+ REVISION = %q$Revision: 1b8226c06192 $
28
+
29
+
30
+ require 'arrow/constants'
31
+ require 'arrow/monkeypatches'
32
+ require 'arrow/exceptions'
33
+ require 'arrow/mixins'
34
+ require 'arrow/logger'
35
+
36
+
37
+ # Hook up PluginFactory logging to Arrow logging
38
+ PluginFactory.logger_callback = lambda do |lvl, msg|
39
+ Arrow::Logger[PluginFactory].debug( msg )
40
+ end
41
+ PluginFactory.log.debug( "Hooked up PluginFactory logging through Arrow's logger." )
42
+
43
+
44
+ ### Return the library's version string
45
+ def self::version_string( include_buildnum=false )
46
+ vstring = "%s %s" % [ self.name, VERSION ]
47
+ vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
48
+ return vstring
49
+ end
50
+
51
+ end # module Arrow
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'arrow'
4
+ require 'arrow/exceptions'
5
+ require 'arrow/mixins'
6
+
7
+
8
+ # Arrow::AcceptParam -- a parser for Accept headers, allowing for
9
+ # weighted and wildcard comparisions.
10
+ #
11
+ # == Synopsis
12
+ #
13
+ # require 'arrow/acceptparam'
14
+ # ap = Arrow::AcceptParam.parse( "text/html;q=0.9;level=2" )
15
+ #
16
+ # ap.type #=> 'text'
17
+ # ap.subtype #=> 'html'
18
+ # ap.qvalue #=> 0.9
19
+ # ap =~ 'text/*' #=> true
20
+ #
21
+ # == Authors
22
+ #
23
+ # This class was originally written as part of the ThingFish project.
24
+ #
25
+ # * Michael Granger <ged@FaerieMUD.org>
26
+ # * Mahlon E. Smith <mahlon@martini.nu>
27
+ #
28
+ # Please see the file LICENSE in the top-level directory for licensing details.
29
+ #
30
+ class Arrow::AcceptParam
31
+ include Comparable,
32
+ Arrow::Loggable
33
+
34
+ # The default quality value (weight) if none is specified
35
+ Q_DEFAULT = 1.0
36
+
37
+ # The maximum quality value
38
+ Q_MAX = Q_DEFAULT
39
+
40
+
41
+ ### Parse the given +accept_param+ and return an AcceptParam object.
42
+ def self::parse( accept_param )
43
+ raise Arrow::RequestError, "Bad Accept param: no media-range" unless
44
+ accept_param =~ %r{/}
45
+ media_range, *stuff = accept_param.split( /\s*;\s*/ )
46
+ type, subtype = media_range.downcase.split( '/', 2 )
47
+ qval, opts = stuff.partition {|par| par =~ /^q\s*=/ }
48
+
49
+ return new( type, subtype, qval.first, *opts )
50
+ end
51
+
52
+
53
+ ### Create a new Arrow::AcceptParam with the given media +range+, quality value
54
+ ### (+qval+), and extensions
55
+ def initialize( type, subtype, qval=Q_DEFAULT, *extensions )
56
+ type = nil if type == '*'
57
+ subtype = nil if subtype == '*'
58
+
59
+ @type = type
60
+ @subtype = subtype
61
+ @qvalue = normalize_qvalue( qval )
62
+ @extensions = extensions.flatten
63
+ end
64
+
65
+
66
+ ######
67
+ public
68
+ ######
69
+
70
+ # The 'type' part of the media range
71
+ attr_reader :type
72
+
73
+ # The 'subtype' part of the media range
74
+ attr_reader :subtype
75
+
76
+ # The weight of the param
77
+ attr_reader :qvalue
78
+
79
+ # An array of any accept-extensions specified with the parameter
80
+ attr_reader :extensions
81
+
82
+
83
+ ### Match operator -- returns true if +other+ (an AcceptParan or something
84
+ ### that can to_s to a mime type) is a mime type which matches the receiving
85
+ ### AcceptParam.
86
+ def =~( other )
87
+ unless other.is_a?( Arrow::AcceptParam )
88
+ other = self.class.parse( other.to_s ) rescue nil
89
+ return false unless other
90
+ end
91
+
92
+ # */* returns true in either side of the comparison.
93
+ # ASSUMPTION: There will never be a case when a type is wildcarded
94
+ # and the subtype is specific. (e.g., */xml)
95
+ # We gave up trying to read RFC 2045.
96
+ return true if other.type.nil? || self.type.nil?
97
+
98
+ # text/html =~ text/html
99
+ # text/* =~ text/html
100
+ # text/html =~ text/*
101
+ if other.type == self.type
102
+ return true if other.subtype.nil? || self.subtype.nil?
103
+ return true if other.subtype == self.subtype
104
+ end
105
+
106
+ return false
107
+ end
108
+
109
+
110
+ ### Return a human-readable version of the object
111
+ def inspect
112
+ return "#<%s:0x%07x '%s/%s' q=%0.3f %p>" % [
113
+ self.class.name,
114
+ self.object_id * 2,
115
+ self.type || '*',
116
+ self.subtype || '*',
117
+ self.qvalue,
118
+ self.extensions,
119
+ ]
120
+ end
121
+
122
+
123
+ ### Return the parameter as a String suitable for inclusion in an Accept
124
+ ### HTTP header
125
+ def to_s
126
+ return [
127
+ self.mediatype,
128
+ self.qvaluestring,
129
+ self.extension_strings
130
+ ].compact.join(';')
131
+ end
132
+
133
+
134
+ ### The mediatype of the parameter, consisting of the type and subtype
135
+ ### separated by '/'.
136
+ def mediatype
137
+ return "%s/%s" % [ self.type || '*', self.subtype || '*' ]
138
+ end
139
+ alias_method :mimetype, :mediatype
140
+ alias_method :content_type, :mediatype
141
+
142
+
143
+ ### The weighting or "qvalue" of the parameter in the form "q=<value>"
144
+ def qvaluestring
145
+ # 3 digit precision, trim excess zeros
146
+ return sprintf( "q=%0.3f", self.qvalue ).gsub(/0{1,2}$/, '')
147
+ end
148
+
149
+
150
+ ### Return a String containing any extensions for this parameter, joined
151
+ ### with ';'
152
+ def extension_strings
153
+ return nil if @extensions.empty?
154
+ return @extensions.compact.join('; ')
155
+ end
156
+
157
+
158
+ ### Comparable interface. Sort parameters by weight: Returns -1 if +other+
159
+ ### is less specific than the receiver, 0 if +other+ is as specific as
160
+ ### the receiver, and +1 if +other+ is more specific than the receiver.
161
+ def <=>( other )
162
+
163
+ if rval = (other.qvalue <=> @qvalue).nonzero?
164
+ return rval
165
+ end
166
+
167
+ if @type.nil?
168
+ return 1 if ! other.type.nil?
169
+ elsif other.type.nil?
170
+ return -1
171
+ end
172
+
173
+ if @subtype.nil?
174
+ return 1 if ! other.subtype.nil?
175
+ elsif other.subtype.nil?
176
+ return -1
177
+ end
178
+
179
+ if rval = (other.extensions.length <=> @extensions.length).nonzero?
180
+ return rval
181
+ end
182
+
183
+ return self.mediatype <=> other.mediatype
184
+ end
185
+
186
+
187
+ #######
188
+ private
189
+ #######
190
+
191
+ ### Given an input +qvalue+, return the Float equivalent.
192
+ def normalize_qvalue( qvalue )
193
+ return Q_DEFAULT unless qvalue
194
+ qvalue = Float( qvalue.to_s.sub(/q=/, '') ) unless qvalue.is_a?( Float )
195
+
196
+ if qvalue > Q_MAX
197
+ self.log.notice "Squishing invalid qvalue %p to %0.1f" %
198
+ [ qvalue, Q_DEFAULT ]
199
+ return Q_DEFAULT
200
+ end
201
+
202
+ return qvalue
203
+ end
204
+
205
+ end # Arrow::AcceptParam
206
+
207
+ # vim: set nosta noet ts=4 sw=4:
@@ -0,0 +1,725 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arrow/mixins'
4
+ require 'arrow/exceptions'
5
+ require 'arrow/object'
6
+ require 'arrow/formvalidator'
7
+ require 'arrow/transaction'
8
+
9
+ # An abstract base class for Arrow applets. Provides execution logic,
10
+ # argument-parsing/untainting/validation, and templating through an injected
11
+ # factory.
12
+ #
13
+ # == Synopsis
14
+ #
15
+ # require 'arrow/applet'
16
+ #
17
+ # class MyApplet < Arrow::Applet
18
+ # applet_name "My Applet"
19
+ # applet_description 'Displays a block of whatever character is ' +
20
+ # 'passed as argument'
21
+ # applet_maintainer 'Michael Granger <mgranger@rubycrafters.com>'
22
+ # applet_version '1.01'
23
+ # default_action :form
24
+ #
25
+ # # Define the 'display' action
26
+ # def_action :display do |txn|
27
+ # char = txn.vargs[:char] || 'x'
28
+ # char_page = self.make_character_page( char )
29
+ # templ = self.load_template( :main )
30
+ # templ.char_page = char_page
31
+ #
32
+ # return templ
33
+ # end
34
+ # template :main, "main.tmpl"
35
+ #
36
+ # # Define the 'form' action -- display a form that can be used to set
37
+ # # the character the block is composed of. Save the returned proxy so
38
+ # # the related signature values can be set.
39
+ # formaction = def_action :form do |txn|
40
+ # templ = self.load_template( :form )
41
+ # templ.txn = txn
42
+ # return templ
43
+ # end
44
+ # formaction.template = "form.tmpl"
45
+ #
46
+ # # Make a page full of character +char+.
47
+ # def make_character_page( char )
48
+ # page = ''
49
+ # 40.times do
50
+ # page << (char * 80) << "\n"
51
+ # end
52
+ # end
53
+ #
54
+ # end
55
+ #
56
+ # == Subversion Id
57
+ #
58
+ # $Id$
59
+ #
60
+ # == Authors
61
+ #
62
+ # * Michael Granger <ged@FaerieMUD.org>
63
+ #
64
+ # :include: LICENSE
65
+ #
66
+ #--
67
+ #
68
+ # Please see the file LICENSE in the BASE directory for licensing details.
69
+ #
70
+ class Arrow::Applet < Arrow::Object
71
+
72
+
73
+ ### Applet signature struct. The fields are as follows:
74
+ ### [<b>name</b>]
75
+ ### The name of the applet; used for introspection and reports.
76
+ ### [<b>description</b>]
77
+ ### The description of the applet; used for introspection.
78
+ ### [<b>maintainer</b>]
79
+ ### The name of the maintainer for reports and introspection.
80
+ ### [<b>version</b>]
81
+ ### The version or revision number of the applet, which can be
82
+ ### any object that has a #to_s method.
83
+ ### [<b>default_action</b>]
84
+ ### The action that will be run if no action is specified.
85
+ ### [<b>templates</b>]
86
+ ### A hash of templates used by the applet. The keys are Symbol
87
+ ### identifiers which will be used for lookup, and the values are the
88
+ ### paths to template files.
89
+ ### [<b>validator_profiles</b>]
90
+ ### A hash containing profiles for the built in form validator, one
91
+ ### per action. See the documentation for FormValidator for the format
92
+ ### of each profile hash.
93
+ SignatureStruct = Struct.new( :name, :description, :maintainer,
94
+ :version, :config, :default_action, :templates, :validator_profiles )
95
+
96
+ # Default-generators for Signatures which are missing one or more of the
97
+ # optional pairs.
98
+ SignatureStructDefaults = {
99
+ :name => proc {|rawsig, klass| klass.name},
100
+ :description => "(none)",
101
+ :maintainer => "", # Workaround for RDoc
102
+ :version => nil, # Workaround for RDoc
103
+ :default_action => '_default',
104
+ :config => {},
105
+ :templates => {},
106
+ :validator_profiles => {
107
+ :__default__ => {
108
+ :optional => [:action],
109
+ :constraints => {
110
+ :action => /^\w+$/,
111
+ },
112
+ },
113
+ }
114
+ }
115
+
116
+ SignatureStructDefaults[:version] = proc {|rawsig, klass|
117
+ if klass.const_defined?( :SVNRev )
118
+ return klass.const_get( :SVNRev ).gsub(/Rev: /, 'r')
119
+ elsif klass.const_defined?( :Version )
120
+ return klass.const_get( :Version )
121
+ elsif klass.const_defined?( :VERSION )
122
+ return klass.const_get( :VERSION )
123
+ elsif klass.const_defined?( :REVISION )
124
+ return klass.const_get( :REVISION )
125
+ elsif klass.const_defined?( :Revision )
126
+ return klass.const_get( :Revision )
127
+ elsif klass.const_defined?( :Rcsid )
128
+ return klass.const_get( :Rcsid )
129
+ else
130
+ begin
131
+ File.stat( klass.sourcefile ).mtime.strftime('%Y%m%d-%M:%H')
132
+ rescue
133
+ end
134
+ end
135
+ }
136
+
137
+
138
+ ### Proxy into the Applet's signature for a given action.
139
+ class SigProxy
140
+
141
+ ### Create a new proxy into the given +klass+'s Signature for the
142
+ ### specified +action_name+.
143
+ def initialize( action_name, klass )
144
+ @action_name = action_name.to_s.to_sym
145
+ @signature = klass.signature
146
+ @signature[:templates] ||= {}
147
+ @signature[:validator_profiles] ||= {}
148
+ end
149
+
150
+
151
+ ### Get the template associated with the same name as the proxied
152
+ ### action.
153
+ def template
154
+ @signature[:templates][@action_name]
155
+ end
156
+
157
+
158
+ ### Set the template associated with the same name as the proxied
159
+ ### action to +tmpl+.
160
+ def template=( tmpl )
161
+ @signature[:templates][@action_name] = tmpl
162
+ end
163
+
164
+
165
+ ### Get the validator profile associated with the same name as the
166
+ ### proxied action.
167
+ def validator_profile
168
+ @signature[:validator_profiles][@action_name]
169
+ end
170
+
171
+
172
+ ### Set the validator profile associated with the same name as the
173
+ ### proxied action to +hash+.
174
+ def validator_profile=( hash )
175
+ @signature[:validator_profiles][@action_name] = hash
176
+ end
177
+
178
+ end # class SigProxy
179
+
180
+
181
+ # The array of loaded applet classes (derivatives) and an array of
182
+ # newly-loaded ones.
183
+ @derivatives = []
184
+ @newly_loaded = []
185
+
186
+
187
+ #############################################################
188
+ ### C L A S S M E T H O D S
189
+ #############################################################
190
+
191
+ class << self
192
+ # The Array of loaded applet classes (derivatives)
193
+ attr_reader :derivatives
194
+
195
+ # The Array of applet classes that were loaded by the most recent call
196
+ # to .load.
197
+ attr_reader :newly_loaded
198
+
199
+ # The file containing the applet's class definition
200
+ attr_accessor :filename
201
+ end
202
+
203
+
204
+ ### Set the path for the template specified by +sym+ to +path+.
205
+ def self::template( sym, path=nil )
206
+ case sym
207
+ when Symbol, String
208
+ self.signature.templates[ sym ] = path
209
+
210
+ when Hash
211
+ self.signature.templates.merge!( sym )
212
+
213
+ else
214
+ raise ArgumentError, "cannot convert %s to Symbol" % [ sym ]
215
+ end
216
+ end
217
+ class << self
218
+ # Allow either 'template' or 'templates'
219
+ alias_method :templates, :template
220
+ end
221
+
222
+
223
+ ### Set the name of the applet to +name+.
224
+ def self::applet_name( name )
225
+ self.signature.name = name
226
+ end
227
+
228
+
229
+ ### Set the description of the applet to +desc+.
230
+ def self::applet_description( desc )
231
+ self.signature.description = desc
232
+ end
233
+
234
+
235
+ ### Set the contact information for the maintainer of the applet to +info+.
236
+ def self::applet_maintainer( info )
237
+ self.signature.maintainer = info
238
+ end
239
+
240
+
241
+ ### Set the contact information for the maintainer of the applet to +info+.
242
+ def self::applet_version( ver )
243
+ self.signature.version = ver
244
+ end
245
+
246
+
247
+ ### Set the default action for the applet to +action+.
248
+ def self::default_action( action )
249
+ self.signature.default_action = action.to_s
250
+ end
251
+ deprecate_class_method :applet_default_action, :default_action
252
+
253
+
254
+ ### Set the validator +rules+ for the specified +action+.
255
+ def self::validator( action, rules={} )
256
+ if action.is_a?( Hash ) && rules.empty?
257
+ Arrow::Logger[ self ].debug "Assuming hash syntax for validation definition: %p" % [ action ]
258
+ action, rules = *action.to_a.first
259
+ end
260
+ Arrow::Logger[ self ].debug "Defining validator for action %p with rules %p" % [ action, rules ]
261
+ self.signature.validator_profiles[ action ] = rules
262
+ end
263
+
264
+
265
+ ### Inheritance callback: register any derivative classes so they can be
266
+ ### looked up later.
267
+ def self::inherited( klass )
268
+ @inherited_from = true
269
+ if defined?( @newly_loaded )
270
+ @newly_loaded.push( klass )
271
+ super
272
+ else
273
+ Arrow::Applet.inherited( klass )
274
+ end
275
+ end
276
+
277
+
278
+ ### Have any subclasses of this class been created?
279
+ def self::inherited_from?
280
+ @inherited_from
281
+ end
282
+
283
+
284
+ ### Method definition callback: Check newly-defined action methods for
285
+ ### appropriate arity.
286
+ def self::method_added( sym )
287
+ if /^(\w+)_action$/.match( sym.to_s ) &&
288
+ self.instance_method( sym ).arity.zero?
289
+ raise ScriptError, "Inappropriate arity for #{sym}", caller(1)
290
+ end
291
+ end
292
+
293
+
294
+ ### Load any applet classes in the given file and return them. Ignores
295
+ ### any class which has a subclass in the file unless +include_base_classes+
296
+ ### is set false
297
+ def self::load( filename, include_base_classes=false )
298
+ self.newly_loaded.clear
299
+
300
+ # Load the applet file in an anonymous module. Any applet classes get
301
+ # collected via the ::inherited hook into @newly_loaded
302
+ Kernel.load( filename, true )
303
+
304
+ newderivatives = @newly_loaded.dup
305
+ @derivatives -= @newly_loaded
306
+ @derivatives.push( *@newly_loaded )
307
+
308
+ newderivatives.each do |applet|
309
+ applet.filename = filename
310
+ end
311
+
312
+ unless include_base_classes
313
+ newderivatives.delete_if do |applet|
314
+ applet.inherited_from?
315
+ end
316
+ end
317
+
318
+ return newderivatives
319
+ end
320
+
321
+
322
+ ### Return the name of the applet class after stripping off any
323
+ ### namespace-safe prefixes.
324
+ def self::normalized_name
325
+ self.name.sub( /#<Module:0x\w+>::/, '' )
326
+ end
327
+
328
+
329
+ ### Get the applet's signature (an
330
+ ### Arrow::Applet::SignatureStruct object).
331
+ def self::signature
332
+ @signature ||= make_signature()
333
+ end
334
+
335
+
336
+ ### Returns +true+ if the applet class has a signature.
337
+ def self::signature?
338
+ !self.signature.nil?
339
+ end
340
+
341
+
342
+ ### Signature lookup: look for either a constant or an instance
343
+ ### variable of the class that contains the raw signature hash, and
344
+ ### convert it to an Arrow::Applet::SignatureStruct object.
345
+ def self::make_signature
346
+ rawsig = nil
347
+ if self.instance_variables.include?( "@signature" )
348
+ rawsig = self.instance_variable_get( :@signature )
349
+ elsif self.constants.include?( "Signature" )
350
+ rawsig = self.const_get( :Signature )
351
+ elsif self.constants.include?( "SIGNATURE" )
352
+ rawsig = self.const_get( :SIGNATURE )
353
+ else
354
+ rawsig = {}
355
+ end
356
+
357
+ # Backward-compatibility: Rewrite the 'vargs' member as
358
+ # 'validator_profiles' if 'vargs' exists and 'validator_profiles'
359
+ # doesn't. 'vargs' member will be deleted regardless.
360
+ rawsig[ :validator_profiles ] ||= rawsig.delete( :vargs ) if
361
+ rawsig.key?( :vargs )
362
+
363
+ # If the superclass has a signature, inherit values from it for
364
+ # pairs that are missing.
365
+ if self.superclass < Arrow::Applet && self.superclass.signature?
366
+ self.superclass.signature.each_pair do |member,value|
367
+ next if [:name, :description, :version].include?( member )
368
+ if rawsig[member].nil?
369
+ rawsig[ member ] = value.dup rescue value
370
+ end
371
+ end
372
+ end
373
+
374
+ # Apply sensible defaults for members that aren't defined
375
+ SignatureStructDefaults.each do |key,val|
376
+ next if rawsig[ key ]
377
+ case val
378
+ when Proc, Method
379
+ rawsig[ key ] = val.call( rawsig, self )
380
+ when Numeric, NilClass, FalseClass, TrueClass
381
+ rawsig[ key ] = val
382
+ else
383
+ rawsig[ key ] = val.dup
384
+ end
385
+ end
386
+
387
+ # Signature = Struct.new( :name, :description, :maintainer,
388
+ # :version, :config, :default_action, :templates, :validatorArgs,
389
+ # :monitors )
390
+ members = SignatureStruct.members.collect {|m| m.to_sym}
391
+ return SignatureStruct.new( *rawsig.values_at(*members) )
392
+ end
393
+
394
+
395
+ ### Define an action for the applet. Transactions which include the
396
+ ### specified +name+ as the first directory of the uri after the one the
397
+ ### applet is assigned to will be passed to the given +block+. The
398
+ ### return value from this method is an Arrow::Applet::SigProxy which
399
+ ### can be used to set associated values in the applet's Signature; see
400
+ ### the Synopsis in lib/arrow/applet.rb for examples of how to use this.
401
+ def self::def_action( name, &block )
402
+ name = '_default' if name.to_s.empty?
403
+
404
+ # Action must accept at least a transaction argument
405
+ unless block.arity.nonzero?
406
+ raise ScriptError,
407
+ "Malformed action #{name}: must accept at least one argument"
408
+ end
409
+
410
+ methodName = "#{name}_action"
411
+ define_method( methodName, &block )
412
+ SigProxy.new( name, self )
413
+ end
414
+
415
+
416
+ deprecate_class_method :action, :def_action
417
+
418
+
419
+
420
+ #############################################################
421
+ ### I N S T A N C E M E T H O D S
422
+ #############################################################
423
+
424
+ ### Create a new Arrow::Applet object with the specified +config+ (an
425
+ ### Arrow::Config object), +template_factory+ (an Arrow::TemplateFactory
426
+ ### object), and the +uri+ the applet will live under in the appserver (a
427
+ ### String).
428
+ def initialize( config, template_factory, uri )
429
+ @config = config
430
+ @template_factory = template_factory
431
+ @uri = uri
432
+
433
+ @signature = self.class.signature.dup
434
+ @run_count = 0
435
+ @total_utime = 0
436
+ @total_stime = 0
437
+
438
+ # Make a regexp out of all public <something>_action methods
439
+ @actions = self.public_methods( true ).
440
+ select {|meth| /^(\w+)_action$/ =~ meth }.
441
+ collect {|meth| meth.gsub(/_action/, '') }
442
+ @actions_regexp = Regexp.new( "^(" + actions.join( '|' ) + ")$" )
443
+ end
444
+
445
+
446
+ ######
447
+ public
448
+ ######
449
+
450
+ # The Arrow::Config object which contains the system's configuration.
451
+ attr_accessor :config
452
+
453
+ # The URI the applet answers to
454
+ attr_reader :uri
455
+
456
+ # The Struct that contains the configuration values for this applet
457
+ attr_reader :signature
458
+
459
+ # The number of times this particular applet object has been run
460
+ attr_reader :run_count
461
+
462
+ # The number of user seconds spent in this applet's #run method.
463
+ attr_reader :total_utime
464
+
465
+ # The number of system seconds spent in this applet's #run method.
466
+ attr_reader :total_stime
467
+
468
+ # The Arrow::TemplateFactory object used to load templates for the applet.
469
+ attr_reader :template_factory
470
+
471
+ # The list of all valid actions on the applet
472
+ attr_reader :actions
473
+
474
+
475
+ ### Run the specified +action+ for the given +txn+ and the specified
476
+ ### +args+.
477
+ def run( txn, *args )
478
+ self.log.debug "Running %s" % [ self.signature.name ]
479
+
480
+ return self.time_request do
481
+ name, *newargs = self.get_action_name( txn, *args )
482
+ txn.vargs = self.make_validator( name, txn )
483
+ action = self.find_action_method( txn, name, *newargs )
484
+
485
+ # Decline the request if the action isn't a callable object
486
+ unless action.respond_to?( :arity )
487
+ self.log.info "action method (%p) doesn't do #arity, returning it as-is." %
488
+ [ action ]
489
+ return action
490
+ end
491
+
492
+ self.log.debug "calling action method %p with args (%p)" % [ action, newargs ]
493
+ self.call_action_method( txn, action, *newargs )
494
+ end
495
+ end
496
+
497
+
498
+ ### Wrapper method for a delegation (chained) request.
499
+ def delegate( txn, chain, *args )
500
+ yield( chain )
501
+ end
502
+
503
+
504
+ ### Returns +true+ if the receiver has a #delegate method that is inherited
505
+ ### from somewhere other than the base Arrow::Applet class.
506
+ def delegable?
507
+ return self.method(:delegate).to_s !~ /\(Arrow::Applet\)/
508
+ end
509
+ alias_method :chainable?, :delegable?
510
+
511
+
512
+ ### The action invoked if the specified action is not explicitly
513
+ ### defined. The default implementation will look for a template with the
514
+ ### same key as the action, and if found, will load that and return it.
515
+ def action_missing_action( txn, raction, *args )
516
+ self.log.debug "In action_missing_action with: raction = %p, args = %p" %
517
+ [ raction, args ]
518
+
519
+ if raction && raction.to_s =~ /^([a-z]\w+)$/
520
+ tmplkey = $1.untaint
521
+ self.log.debug "tmpl is: %p (%stainted)" %
522
+ [ tmplkey, tmplkey.tainted? ? "" : "not " ]
523
+
524
+ if @signature.templates.key?( tmplkey.to_sym )
525
+ self.log.debug "Using template sender default action for %s" % raction
526
+ txn.vargs = self.make_validator( tmplkey, txn )
527
+
528
+ tmpl = self.load_template( raction.to_sym )
529
+ tmpl.txn = txn
530
+ tmpl.applet = self
531
+
532
+ return tmpl
533
+ end
534
+ end
535
+
536
+ raise Arrow::AppletError, "No such action '%s' in %s" %
537
+ [ raction, self.signature.name ]
538
+ end
539
+
540
+
541
+ ### Return a human-readable String representing the applet.
542
+ def inspect
543
+ "<%s:0x%08x: %s [%s/%s]>" % [
544
+ self.class.name,
545
+ self.object_id * 2,
546
+ @signature.name,
547
+ @signature.version,
548
+ @signature.maintainer
549
+ ]
550
+ end
551
+
552
+
553
+ ### Returns the average number of seconds (user + system) per run.
554
+ def average_usage
555
+ return 0.0 if @run_count.zero?
556
+ (@total_utime + @total_stime) / @run_count.to_f
557
+ end
558
+
559
+
560
+ #########
561
+ protected
562
+ #########
563
+
564
+ ### Time the block, logging the result
565
+ def time_request
566
+ starttimes = Process.times
567
+ yield
568
+ ensure
569
+ runtimes = Process.times
570
+ @run_count += 1
571
+ @total_utime += utime = (runtimes.utime - starttimes.utime)
572
+ @total_stime += stime = (runtimes.stime - starttimes.stime)
573
+ self.log.info \
574
+ "[PID %d] Runcount: %d, User: %0.2f/%0.2f, System: %0.2f/%0.2f" %
575
+ [ Process.pid, @run_count, utime, @total_utime, stime, @total_stime ]
576
+ end
577
+
578
+
579
+ ### Get the expected action name from the specified +txn+ and the +args+ extracted from the
580
+ ### URI path; return the action as a Symbol and the remaining arguments as a splatted Array.
581
+ def get_action_name( txn, *args )
582
+ self.log.debug "Fetching the intended action name from txn = %p, args = %p" % [ txn, args ]
583
+
584
+ name = args.shift
585
+ name = nil if name.to_s.empty?
586
+ name ||= @signature.default_action or
587
+ raise Arrow::AppletError, "Missing default handler"
588
+
589
+ if (( action = self.map_to_valid_action(name) ))
590
+ self.log.debug " found what looks like a valid action (%p)" % [ action ]
591
+ return action.to_sym, *args
592
+ else
593
+ self.log.debug " didn't find a valid action; returning :action_missing"
594
+ return :action_missing, name, *args
595
+ end
596
+ end
597
+
598
+
599
+ ### Map the given +action+ name to an action that's been declared, or else map it to a
600
+ ### fallback action ('action_missing' by default).
601
+ def map_to_valid_action( action )
602
+ self.log.debug "trying to map %p to a valid action method" % [ action ]
603
+
604
+ if (( match = @actions_regexp.match(action.to_s) ))
605
+ action = match.captures[0]
606
+ action.untaint
607
+ self.log.debug "Matched action = #{action}"
608
+ return action
609
+ else
610
+ self.log.debug " no matching action method in %p" % [ @actions_regexp ]
611
+ return nil
612
+ end
613
+ end
614
+
615
+
616
+ ### Given an +action+ name and any other URI path +args+ from the request, return
617
+ ### a Method object that will handle the request, and the remaining arguments
618
+ ### as a splatted Array.
619
+ def find_action_method( txn, action, *args )
620
+ self.log.debug "Mapping %s( %p ) to an action" % [ action, args ]
621
+ return self.method( "#{action}_action" )
622
+ end
623
+
624
+
625
+ ### Invoke the specified +action+ (an object that responds to #arity and #call) with the
626
+ ### given +txn+ and the +args+ which it can accept based on its arity.
627
+ def call_action_method( txn, action, *args )
628
+ self.log.debug "Applet action arity: %d; args = %p" %
629
+ [ action.arity, args ]
630
+
631
+ # Invoke the action with the right number of arguments.
632
+ if action.arity < 0
633
+ return action.call( txn, *args )
634
+ elsif action.arity >= 1
635
+ args.unshift( txn )
636
+ until args.length >= action.arity do args << nil end
637
+ return action.call( *(args[0, action.arity]) )
638
+ else
639
+ raise Arrow::AppletError,
640
+ "Malformed action: Must accept at least a transaction argument"
641
+ end
642
+ end
643
+
644
+
645
+ ### Run an action with a duped transaction (e.g., from another action)
646
+ def subrun( action, txn, *args )
647
+ action, txn = txn, action if action.is_a?( Arrow::Transaction )
648
+
649
+ txn.vargs ||= self.make_validator( action, txn )
650
+ action = self.method( "#{action}_action" ) unless action.respond_to?( :arity )
651
+ self.log.debug "Running subordinate action '%s' from '%s'" %
652
+ [ action, caller[0] ]
653
+
654
+ return self.call_action_method( txn, action, *args )
655
+ end
656
+
657
+
658
+ ### Load and return the template associated with the given +key+ according
659
+ ### to the applet's signature. Returns +nil+ if no such template exists.
660
+ def load_template( key )
661
+ tname = @signature.templates[key] or
662
+ raise Arrow::AppletError,
663
+ "No such template %p defined in the signature for %s (%s)" %
664
+ [ key, self.signature.name, self.class.filename ]
665
+
666
+ tname.untaint
667
+
668
+ return @template_factory.get_template( tname )
669
+ end
670
+ alias_method :template, :load_template
671
+
672
+
673
+ ### Create a FormValidator object for the specified +action+ which has
674
+ ### been given the arguments from the given +txn+.
675
+ def make_validator( action, txn )
676
+ profile = self.get_validator_profile_for_action( action, txn ) or
677
+ return nil
678
+
679
+ # Create a new validator object, map the request args into a regular
680
+ # hash, and then send them to the validaator with the applicable profile
681
+ self.log.debug "Creating form validator for profile: %p" % [ profile ]
682
+
683
+ params = {}
684
+
685
+ # Only try to parse form parameters if there's a form
686
+ if txn.form_request?
687
+ txn.request.paramtable.each do |key,val|
688
+ # Multi-valued vs. single params
689
+ params[key] = val.to_a.length > 1 ? val.to_a : val.to_s
690
+ end
691
+ end
692
+ validator = Arrow::FormValidator.new( profile, params )
693
+
694
+ self.log.debug "Validator: %p" % validator
695
+ return validator
696
+ end
697
+
698
+
699
+ ### Return the validator profile that corresponds to the +action+ which
700
+ ### will be executed by the specified +txn+. Returns the __default__
701
+ ### profile if no more-specific one is available.
702
+ def get_validator_profile_for_action( action, txn )
703
+ if action.to_s =~ /^(\w+)$/
704
+ action = $1
705
+ action.untaint
706
+ else
707
+ self.log.warning "Invalid action '#{action.inspect}'"
708
+ action = :__default__
709
+ end
710
+
711
+ # Look up the profile for the applet or the default one
712
+ profile = @signature.validator_profiles[ action.to_sym ] ||
713
+ @signature.validator_profiles[ :__default__ ]
714
+
715
+ if profile.nil?
716
+ self.log.warning "No validator for #{action}, and no __default__. "\
717
+ "Returning nil validator."
718
+ return nil
719
+ end
720
+
721
+ return profile
722
+ end
723
+
724
+ end # class Arrow::Applet
725
+