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,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'digest/md5'
4
+
5
+ require 'arrow/session/id'
6
+
7
+ # The Arrow::Session::MD5Id class, a derivative of Arrow::Session::Id. Instances
8
+ # of this class are session IDs created by MD5-hashing some semi-random data.
9
+ #
10
+ # == Authors
11
+ #
12
+ # * Michael Granger <ged@FaerieMUD.org>
13
+ #
14
+ # Please see the file LICENSE in the top-level directory for licensing details.
15
+ #
16
+ class Arrow::Session::MD5Id < Arrow::Session::Id
17
+
18
+
19
+ #############################################################
20
+ ### C L A S S M E T H O D S
21
+ #############################################################
22
+
23
+ ### Generate a new id
24
+ def self::generate( uri, request )
25
+ seed = [
26
+ Time.new.to_s,
27
+ Object.new.inspect,
28
+ rand(),
29
+ Process.pid,
30
+ ].join
31
+ return Digest::MD5.hexdigest( Digest::MD5.hexdigest(seed) )
32
+ end
33
+
34
+ ### Returns the validated id if the given id is in the expected form for
35
+ ### this type, or +nil+ if it is not.
36
+ def self::validate( uri, idstr )
37
+ rval = idstr[/^([a-f0-9]{32})$/]
38
+ rval.untaint
39
+ return rval
40
+ end
41
+
42
+ end # class Arrow::Session::MD5Id
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arrow/session/lock'
4
+
5
+ # The Arrow::Session::NullLock class, a derivative of
6
+ # Arrow::Session::Lock. This is a null lock, in that it does not lock.
7
+ # This is to be used with an ActiveRecord session store that uses
8
+ # Optomistic Concurrency Control.
9
+ #
10
+ # == VCS Id
11
+ #
12
+ # $Id$
13
+ #
14
+ # == Authors
15
+ #
16
+ # * Jeremiah Jordan <phaedrus@FaerieMUD.org>
17
+ #
18
+ # Please see the file LICENSE in the top-level directory for licensing details.
19
+ #
20
+ class Arrow::Session::NullLock < Arrow::Session::Lock
21
+
22
+ def initialize( uri, id )
23
+ super
24
+ end
25
+
26
+ def acquire_read_lock(blocking)
27
+ true
28
+ end
29
+
30
+ def acquire_write_lock(blocking)
31
+ true
32
+ end
33
+
34
+ def release_read_lock
35
+ true
36
+ end
37
+
38
+ def release_write_lock
39
+ true
40
+ end
41
+
42
+ end
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'posixlock'
4
+ require 'ftools'
5
+
6
+ require 'arrow/session/lock'
7
+
8
+ # The Arrow::Session::PosixLock class, a derivative of
9
+ # Arrow::Session::Lock. This lock type uses the 'posixlock' library
10
+ # (http://raa.ruby-lang.org/project/posixlock/).
11
+ #
12
+ # == VCS Id
13
+ #
14
+ # $Id$
15
+ #
16
+ # == Authors
17
+ #
18
+ # * Michael Granger <ged@FaerieMUD.org>
19
+ #
20
+ # Please see the file LICENSE in the top-level directory for licensing details.
21
+ #
22
+ class Arrow::Session::PosixLock < Arrow::Session::Lock
23
+
24
+
25
+ # The path to the default lockdir
26
+ DefaultLockDir = '/tmp'
27
+
28
+ # The format string that will be used for the name of the lock file. The
29
+ # first '%s' will be replaced with a sanitized version of the session
30
+ # id.
31
+ LockfileFormat = "arrow-session-%s.plock"
32
+
33
+ # The mode to open the lockfile in
34
+ FileMode = File::CREAT|File::RDWR
35
+
36
+
37
+ #############################################################
38
+ ### C L A S S M E T H O D S
39
+ #############################################################
40
+
41
+ ### Clean the specified +directory+ of lock files older than +threshold+
42
+ ### seconds.
43
+ def self::clean( directory=DefaultLockDir, threshold=3600 )
44
+ pat = File.join( directory, LockfileFormat.gsub(/%s/, '*') )
45
+ threshold = Time.now - threshold
46
+ Dir[ pat ].each do |file|
47
+ if File.mtime( file ) < threshold
48
+ Arrow::Logger[self].info \
49
+ "Removing stale lockfile '%s'" % file
50
+ begin
51
+ fh = File.open( file, FileMode )
52
+ fh.posixlock( File::LOCK_EX|File::LOCK_NB )
53
+ File.delete( file )
54
+ fh.posixlock( File::LOCK_UN )
55
+ fh.close
56
+ rescue => err
57
+ Arrow::Logger[self].warning \
58
+ "Could not clean up '%s': %s" %
59
+ [ file, err.message ]
60
+ next
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+
67
+
68
+ #############################################################
69
+ ### I N S T A N C E M E T H O D S
70
+ #############################################################
71
+
72
+ ### Create a new Arrow::Session::FileLock object.
73
+ def initialize( uri, id )
74
+ @lockDir = uri.path || DefaultLockDir
75
+ super
76
+
77
+ File.mkpath( @lockDir )
78
+ @filename = File.join( @lockDir, LockfileFormat % id.to_s.gsub(/\W/, '_') ).untaint
79
+ self.log.debug "Filename is: #@filename"
80
+ @lockfile = nil
81
+ end
82
+
83
+
84
+ ######
85
+ public
86
+ ######
87
+
88
+ # The path to the directory where session lockfiles are kept.
89
+ attr_accessor :lockDir
90
+
91
+
92
+ ### Indicate to the lock that the caller will no longer be using it, and it
93
+ ### may free any resources it had been using.
94
+ def finish
95
+ super
96
+ self.close_lock_file
97
+ end
98
+
99
+
100
+
101
+ #########
102
+ protected
103
+ #########
104
+
105
+ ### Get the File object for the lockfile belonging to this lock,
106
+ ### creating it if necessary.
107
+ def lockfile
108
+ @lockfile ||= File.open( @filename, FileMode )
109
+ end
110
+
111
+
112
+ ### Close the lockfile and destroy the File object belonging to this
113
+ ### lock.
114
+ def close_lock_file
115
+ if @lockfile
116
+ path = @lockfile.path
117
+ @lockfile.close
118
+ @lockfile = nil
119
+ if File.exist?( path.untaint )
120
+ File.delete( path.untaint )
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ ### Acquire a read (shared) lock on the lockfile.
127
+ def acquire_read_lock( blocking )
128
+ flags = File::LOCK_SH
129
+ flags |= File::LOCK_NB if !blocking
130
+
131
+ self.lockfile.posixlock( flags )
132
+ end
133
+
134
+
135
+ ### Acquire a write (exclusive) lock on the lockfile.
136
+ def acquire_write_lock( blocking )
137
+ flags = File::LOCK_EX
138
+ flags |= File::LOCK_NB if !blocking
139
+
140
+ self.lockfile.posixlock( flags )
141
+ end
142
+
143
+
144
+ ### Release a previously-acquired read lock.
145
+ def release_read_lock
146
+ if !self.write_locked?
147
+ self.lockfile.posixlock( File::LOCK_UN )
148
+ self.close_lock_file
149
+ end
150
+ end
151
+
152
+
153
+ ### Release a previously-acquired write lock.
154
+ def release_write_lock
155
+ if self.read_locked?
156
+ self.lockfile.posixlock( File::LOCK_SH )
157
+ else
158
+ self.lockfile.posixlock( File::LOCK_UN )
159
+ self.close_lock_file
160
+ end
161
+ end
162
+
163
+ end # module Arrow::Session::PosixLock
164
+
165
+
166
+
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'arrow/session/id'
6
+
7
+ # The Arrow::Session::SHA1Id class, a derivative of
8
+ # Arrow::Session::Id. Instances of this class are session IDs created by
9
+ # SHA1-hashing some semi-random data.
10
+ #
11
+ # == Synopsis
12
+ #
13
+ # # In arrow.conf:
14
+ # session:
15
+ # idType: sha1:.
16
+ #
17
+ # == Authors
18
+ #
19
+ # * Michael Granger <ged@FaerieMUD.org>
20
+ #
21
+ # Please see the file LICENSE in the top-level directory for licensing details.
22
+ #
23
+ class Arrow::Session::SHA1Id < Arrow::Session::Id
24
+
25
+ # Default salt characters
26
+ DEFAULT_SALT = 'sadblkw456jbhgsdfi7283hnehonaseegop26m'
27
+
28
+
29
+ #############################################################
30
+ ### C L A S S M E T H O D S
31
+ #############################################################
32
+
33
+ ### Generate a new id
34
+ def self::generate( uri, request )
35
+ salt = uri.opaque || DEFAULT_SALT
36
+ seed = [
37
+ salt,
38
+ Time.new.to_s,
39
+ Object.new.inspect,
40
+ rand(),
41
+ Process.pid,
42
+ ].join
43
+ return Digest::SHA1.hexdigest( Digest::SHA1.hexdigest(seed) )
44
+ end
45
+
46
+ ### Returns the validated id if the given id is in the expected form for
47
+ ### this type, or +nil+ if it is not.
48
+ def self::validate( uri, idstr )
49
+ rval = idstr[/^([a-f0-9]{40})$/]
50
+ rval.untaint
51
+ return rval
52
+ end
53
+
54
+ end # class Arrow::Session::MD5Id
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'forwardable'
4
+ require 'pluginfactory'
5
+
6
+ require 'arrow/mixins'
7
+ require 'arrow/exceptions'
8
+ require 'arrow/session'
9
+ require 'arrow/session/lock'
10
+
11
+ # The Arrow::Session::Store class, a derivative of Arrow::Object. Instances
12
+ # of concrete deriviatives of this class provide serialization and semi-permanent
13
+ # storage of session data for Arrow::Session objects.
14
+ #
15
+ # === Derivative Interface ===
16
+ #
17
+ # In order to create your own session store classes, you need to provide four
18
+ # methods: #insert, #update, #retrieve, #remove. All but one of the methods
19
+ # provides serialization and marking records as dirty in the base class, so
20
+ # unless you want to manage these tasks yourself, you should +super()+ to the
21
+ # parent's implementation with a block. Examples are provided for each method.
22
+ #
23
+ # #insert::
24
+ # Insert a new session into the backing store. Example:
25
+ # def insert
26
+ # super {|data| @io.print(data) }
27
+ # end
28
+ #
29
+ # #update::
30
+ # Update an existing session's data in the backing store. Example:
31
+ # def update
32
+ # super {|data| @io.rewind; @io.truncate(0); @io.print(data) }
33
+ # end
34
+ #
35
+ # #retrieve::
36
+ # Retrieve the serialized session data from the backing store. Example:
37
+ # def retrieve
38
+ # super { @io.rewind; @io.read }
39
+ # end
40
+ #
41
+ # #delete::
42
+ # Delete the session from the backing store. Example:
43
+ # def delete
44
+ # super {|data| @io.close; File.delete(@session_file) }
45
+ # end
46
+ #
47
+ # === Optional Derivative Interface ===
48
+ # ==== Serialization ====
49
+ #
50
+ # If you want to use something other than Marshal for object serialization,
51
+ # you can override the protected methods #serialized_data and #serialized_data=
52
+ # to provide your own serialization.
53
+ #
54
+ # #serialized_data::
55
+ # Serialize the data in the instance variable +@data+ and return it.
56
+ # #serialized_data=( serialized )::
57
+ # Deserialize the given +serialized+ data and assign it to @data.
58
+ #
59
+ # Example (serializing to YAML instead of binary):
60
+ # require 'yaml'
61
+ #
62
+ # def serialized_data
63
+ # @data.to_yaml
64
+ # end
65
+ #
66
+ # def serialized_data=( data )
67
+ # @data = YAML.load( data )
68
+ # end
69
+ #
70
+ # ==== Lock Recommendation ====
71
+ #
72
+ # If arrow is configured to use the 'recommended' session lock, your session
73
+ # store can recommend one it knows will work (e.g., if your session store is
74
+ # a database, you can recommend a lock that uses database locking). The simple
75
+ # way to do that is to define a RecommendedLocker constant in your class which
76
+ # contains the URI of the locker you wish to use. If you need more control
77
+ # than the URI can provide, you can also override the #create_recommended_lock
78
+ # method, which should return an instance of the locker that should be used.
79
+ #
80
+ # The method will be given the instantiated Arrow::Session::Id object that
81
+ # identifies the session so that you can derive a filename, primary key, etc.
82
+ #
83
+ # Example:
84
+ #
85
+ # def create_recommended_lock( idobj )
86
+ # return DBITransactionLock.new( idobj.to_s )
87
+ # end
88
+ #
89
+ # == Authors
90
+ #
91
+ # * Michael Granger <ged@FaerieMUD.org>
92
+ #
93
+ # Please see the file LICENSE in the top-level directory for licensing details.
94
+ #
95
+ class Arrow::Session::Store < Arrow::Object
96
+ include PluginFactory
97
+ extend Forwardable
98
+
99
+ # The URI of the lock class recommended for use with this Store.
100
+ RecommendedLocker = URI.parse( 'file:.' )
101
+
102
+ # The methods which are delegate directly to the data hash.
103
+ DelegatedMethods = [
104
+ :[], :default, :default=, :each, :each_key, :each_pair, :each_value,
105
+ :empty?, :fetch, :has_key?, :has_value?, :include?, :index, :invert,
106
+ :keys, :length, :member?, :merge, :rehash, :reject, :select, :size,
107
+ :sort, :to_a, :value?, :values
108
+ ]
109
+
110
+
111
+ #############################################################
112
+ ### C L A S S M E T H O D S
113
+ #############################################################
114
+
115
+ ### Returns the Array of directories to search for derivatives; part of
116
+ ### the PluginFactory interface.
117
+ def self::derivativeDirs
118
+ [ 'arrow/session', 'arrow/session/store' ]
119
+ end
120
+
121
+
122
+ ### Overridden factory method: handle a URI object or a name
123
+ def self::create( uri, idobj )
124
+ uri = Arrow::Session.parse_uri( uri ) if uri.is_a?( String )
125
+ super( uri.scheme.dup, uri, idobj )
126
+ end
127
+
128
+
129
+
130
+ #############################################################
131
+ ### I N S T A N C E M E T H O D S
132
+ #############################################################
133
+
134
+ ### Create a new Arrow::Session::Store object.
135
+ def initialize( uri, idobj )
136
+ @data = {}
137
+ @id = idobj
138
+ @new = true
139
+ @modified = false
140
+
141
+ unless idobj.new?
142
+ self.retrieve
143
+ end
144
+
145
+ super()
146
+ end
147
+
148
+
149
+ ######
150
+ public
151
+ ######
152
+
153
+ # Delegate some methods to the data hash directly.
154
+ def_delegators :@data, *DelegatedMethods
155
+
156
+
157
+ # The raw session data hash
158
+ attr_reader :data
159
+
160
+
161
+ ### Set the +value+ for the specified +key+.
162
+ def []=( key, value )
163
+ @data[ key ] = value
164
+ @modified = true
165
+ end
166
+ alias_method :store, :[]=
167
+
168
+
169
+ ### Deletes and returns a key-value pair from the receiver whose key is
170
+ ### equal to +key+. If the +key+ is not found, returns the default
171
+ ### value. If the optional code-block is given and the key is not found,
172
+ ### the block is called with the key, and the return value is used as
173
+ ### the result of the method.
174
+ def delete( key, &block )
175
+ rval = @data.delete( key, &block )
176
+ return rval
177
+ ensure
178
+ @modified = true if rval != @data.default
179
+ end
180
+
181
+
182
+ ### Clear all key/value pairs from the store for this session.
183
+ def clear
184
+ @data.clear
185
+ @modified = true
186
+ end
187
+
188
+
189
+ ### Adds the contents of the +other+ hash to the session data,
190
+ ### overwriting entries in the session data with values from the +other+
191
+ ### hash where there are duplicates. If a +block+ is given, it is called
192
+ ### for each duplicate key, and the return value is the value set in the
193
+ ### hash.
194
+ def merge!( other, &block ) # :yields: key, sessionValue, otherValue
195
+ @data.merge!( other, &block )
196
+ ensure
197
+ @modified = true
198
+ end
199
+ alias_method :update, :merge!
200
+
201
+
202
+ ### Replace the contents of the session hash with those of the given
203
+ ### +other+ hash.
204
+ def replace( other )
205
+ @data.replace( other )
206
+ ensure
207
+ @modified = true
208
+ end
209
+
210
+
211
+ ### Deletes every key-value pair from the session data for which the
212
+ ### +block+ evaluates to true.
213
+ def reject!( &block ) # :yields: key, value
214
+ rval = @data.reject!( &block )
215
+ return rval
216
+ ensure
217
+ @modified = true if rval
218
+ end
219
+ alias_method :delete_if, :reject!
220
+
221
+
222
+ ### Returns +true+ if the receiver's data is out of sync with the
223
+ ### data in the backing store.
224
+ def modified?
225
+ @modified
226
+ end
227
+
228
+
229
+ ### Returns +true+ if the data in the receiver has not yet been saved to
230
+ ### the backing store, or if the entry in the backing store has been deleted
231
+ ### since it was last saved.
232
+ def new?
233
+ @new
234
+ end
235
+
236
+
237
+ ### Save the session data to the backing store
238
+ def save
239
+ return false unless self.modified? || self.new?
240
+ if self.new?
241
+ self.insert
242
+ else
243
+ self.update
244
+ end
245
+ end
246
+
247
+
248
+ ### Insert the current +data+ hash into whatever permanent storage the
249
+ ### Store object is acting as an interface to. Concrete implementations
250
+ ### should provide an overriding implementation of this method that
251
+ ### calls #super with a block which will be called with the serialized
252
+ ### data that should be stored.
253
+ def insert
254
+ self.log.debug "Inserting session data for key %s" % @id
255
+ yield( self.serialized_data )
256
+ @new = @modified = false
257
+ end
258
+
259
+
260
+ ### Update the current data hash stored in permanent storage with the
261
+ ### values contained in the store's data. Concrete implementations
262
+ ### should provide an overriding implementation of this method that
263
+ ### calls #super with a block which will be called with the serialized
264
+ ### data that should be stored.
265
+ def update
266
+ self.log.debug "Updating session data for key %s" % @id
267
+ yield( self.serialized_data )
268
+ @modified = false
269
+ end
270
+
271
+
272
+ ### Retrieve the data hash stored in permanent storage associated with
273
+ ### the id the object was created with. Concrete implementations
274
+ ### should provide an overriding implementation of this method that calls
275
+ ### #super with a block that returns the serialized data to be restored.
276
+ def retrieve
277
+ self.log.debug "Retrieving session data for key %s" % @id
278
+ self.serialized_data = yield
279
+ @new = @modified = false
280
+ end
281
+
282
+
283
+ ### Permanently remove the data hash associated with the id used in the
284
+ ### receiver's creation from permanent storage.
285
+ def remove
286
+ self.log.debug "Removing session data for key %s" % @id
287
+ @new = true
288
+ end
289
+
290
+
291
+ ### Returns an instance of the recommended lock object for the receiving
292
+ ### store. If no recommended locking strategy is known, this method
293
+ ### raises a SessionError.
294
+ def create_recommended_lock( idobj )
295
+ self.log.debug "Searching for recommended lock for %s" %
296
+ self.class.name
297
+
298
+ # Traverse the class hierarchy to find a class which defines a
299
+ # RecommendedLocker constant
300
+ adviceClass = self.class.ancestors.find {|klass|
301
+ klass.const_defined?( :RecommendedLocker )
302
+ } or raise SessionError, "No recommended locker for %p" %
303
+ self.class.ancestors
304
+
305
+ uri = adviceClass.const_get( :RecommendedLocker ) or
306
+ raise SessionError, "Could not fetch RecommendedLocker constant"
307
+
308
+ self.log.debug "Creating recommended lock %s" % uri
309
+ uri = Arrow::Session.parse_uri( uri ) if
310
+ uri.is_a?( String )
311
+
312
+ lock = Arrow::Session::Lock.create( uri, idobj )
313
+ self.log.debug "Created recommended lock object: %p" % lock
314
+
315
+ return lock
316
+ end
317
+
318
+
319
+ #########
320
+ protected
321
+ #########
322
+
323
+ ### Returns the data in the session store as a serialized object.
324
+ def serialized_data
325
+ data = strip_hash( @data )
326
+ return Marshal.dump( data )
327
+ end
328
+
329
+
330
+ ### Sets the session's data by deserializing the object
331
+ ### contained in the given +string+.
332
+ def serialized_data=( string )
333
+ if string.empty?
334
+ self.log.error "No session data: retaining default hash"
335
+ else
336
+ @data = Marshal.restore( string )
337
+ end
338
+ end
339
+
340
+
341
+
342
+ #######
343
+ private
344
+ #######
345
+
346
+ ### Return a copy of the given +hash+ with all non-serializable
347
+ ### objects stripped out of it.
348
+ def strip_hash( hash, cloned=true )
349
+ newhash = cloned ? hash.dup : hash
350
+ newhash.default = nil if newhash.default_proc
351
+ newhash.each_key {|key|
352
+ case newhash[ key ]
353
+ when Hash
354
+ newhash[ key ] = strip_hash( newhash[key], false )
355
+
356
+ when Proc, Method, UnboundMethod, IO
357
+ self.log.warning "Stripping unserializable object from session " \
358
+ "hash: %p" % newhash[ key ]
359
+ newhash[ key ] = "[Can't serialize a %s]" % newhash[ key ].class
360
+ end
361
+ }
362
+
363
+ return newhash
364
+ end
365
+
366
+ end # class Arrow::Session::Store