sws 0.4

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 (158) hide show
  1. data/doc/DOC.otl +34 -0
  2. data/doc/Makefile +13 -0
  3. data/doc/architecture.dia +0 -0
  4. data/doc/docbook/architecture.png +0 -0
  5. data/doc/docbook/concepts.docbook +474 -0
  6. data/doc/docbook/installation.docbook +57 -0
  7. data/doc/docbook/introduction.docbook +130 -0
  8. data/doc/docbook/sws_manual.docbook +35 -0
  9. data/doc/docbook/todo.docbook +38 -0
  10. data/doc/docbook/tutorial.docbook +594 -0
  11. data/examples/README +1 -0
  12. data/examples/addressbook/CardBrowse/CardBrowse.html +43 -0
  13. data/examples/addressbook/CardBrowse/CardBrowse.rb +65 -0
  14. data/examples/addressbook/CardBrowse/CardBrowse.sws +92 -0
  15. data/examples/addressbook/Login/LoginPage.html +12 -0
  16. data/examples/addressbook/Login/LoginPage.rb +19 -0
  17. data/examples/addressbook/Login/LoginPage.sws +15 -0
  18. data/examples/addressbook/README +1 -0
  19. data/examples/addressbook/addressbook.rb +70 -0
  20. data/examples/addressbook/application.yaml +8 -0
  21. data/examples/addressbook/db.yaml +7 -0
  22. data/examples/component_demo/CheckBoxDemo/CheckBoxDemo.html +11 -0
  23. data/examples/component_demo/CheckBoxDemo/CheckBoxDemo.rb +21 -0
  24. data/examples/component_demo/CheckBoxDemo/CheckBoxDemo.sws +25 -0
  25. data/examples/component_demo/ComponentDemo.rb +21 -0
  26. data/examples/component_demo/ConditionalDemo/ConditionalDemo.html +18 -0
  27. data/examples/component_demo/ConditionalDemo/ConditionalDemo.rb +2 -0
  28. data/examples/component_demo/ConditionalDemo/ConditionalDemo.sws +22 -0
  29. data/examples/component_demo/FileUploadDemo/FileUploadDemo.html +10 -0
  30. data/examples/component_demo/FileUploadDemo/FileUploadDemo.rb +9 -0
  31. data/examples/component_demo/FileUploadDemo/FileUploadDemo.sws +33 -0
  32. data/examples/component_demo/FormFieldsDemo/FormFieldsDemo.html +12 -0
  33. data/examples/component_demo/FormFieldsDemo/FormFieldsDemo.rb +21 -0
  34. data/examples/component_demo/FormFieldsDemo/FormFieldsDemo.sws +40 -0
  35. data/examples/component_demo/FormListsDemo/FormListsDemo.html +11 -0
  36. data/examples/component_demo/FormListsDemo/FormListsDemo.rb +37 -0
  37. data/examples/component_demo/FormListsDemo/FormListsDemo.sws +47 -0
  38. data/examples/component_demo/GenericDemo/GenericDemo.html +4 -0
  39. data/examples/component_demo/GenericDemo/GenericDemo.rb +2 -0
  40. data/examples/component_demo/GenericDemo/GenericDemo.sws +10 -0
  41. data/examples/component_demo/HyperlinkDemo/HyperlinkDemo.html +8 -0
  42. data/examples/component_demo/HyperlinkDemo/HyperlinkDemo.rb +20 -0
  43. data/examples/component_demo/HyperlinkDemo/HyperlinkDemo.sws +19 -0
  44. data/examples/component_demo/ImageLinkDemo/ImageLinkDemo.html +11 -0
  45. data/examples/component_demo/ImageLinkDemo/ImageLinkDemo.rb +2 -0
  46. data/examples/component_demo/ImageLinkDemo/ImageLinkDemo.sws +14 -0
  47. data/examples/component_demo/PageWrapper/PageWrapper.html +23 -0
  48. data/examples/component_demo/PageWrapper/PageWrapper.rb +2 -0
  49. data/examples/component_demo/PageWrapper/PageWrapper.sws +42 -0
  50. data/examples/component_demo/README +1 -0
  51. data/examples/component_demo/RepetitionDemo/RepetitionDemo.html +13 -0
  52. data/examples/component_demo/RepetitionDemo/RepetitionDemo.rb +19 -0
  53. data/examples/component_demo/RepetitionDemo/RepetitionDemo.sws +20 -0
  54. data/examples/component_demo/StringDemo/StringDemo.html +5 -0
  55. data/examples/component_demo/StringDemo/StringDemo.rb +16 -0
  56. data/examples/component_demo/StringDemo/StringDemo.sws +14 -0
  57. data/examples/component_demo/application.yaml +28 -0
  58. data/examples/component_demo/poweredby.jpg +0 -0
  59. data/examples/component_demo/style.css +1 -0
  60. data/examples/movies/Menu/Menu.html +3 -0
  61. data/examples/movies/Menu/Menu.rb +7 -0
  62. data/examples/movies/Menu/Menu.sws +7 -0
  63. data/examples/movies/MovieBrowse/MovieBrowse.html +68 -0
  64. data/examples/movies/MovieBrowse/MovieBrowse.rb +178 -0
  65. data/examples/movies/MovieBrowse/MovieBrowse.sws +127 -0
  66. data/examples/movies/README +1 -0
  67. data/examples/movies/UserBrowse/UserBrowse.html +50 -0
  68. data/examples/movies/UserBrowse/UserBrowse.rb +69 -0
  69. data/examples/movies/UserBrowse/UserBrowse.sws +49 -0
  70. data/examples/movies/application.yaml +11 -0
  71. data/examples/movies/da.rb +36 -0
  72. data/examples/movies/dbmovies.rb +44 -0
  73. data/examples/movies/frameworks/TestFramework/framework.yaml +4 -0
  74. data/examples/movies/frameworks/TestFramework/resources/im1.jpg +0 -0
  75. data/examples/movies/images/pbr1b.jpg +0 -0
  76. data/examples/movies/movies.rb +28 -0
  77. data/examples/movies/movies.sds +119 -0
  78. data/examples/movies/movies.sqlite +0 -0
  79. data/examples/movies/movies_mysql.sql +28 -0
  80. data/examples/movies/movies_postgres.sql +27 -0
  81. data/examples/movies/movies_sqlite.sql +28 -0
  82. data/lib/sws.rb +89 -0
  83. data/lib/sws/Core/components/CheckBox/CheckBox.api +5 -0
  84. data/lib/sws/Core/components/CheckBox/CheckBox.rb +45 -0
  85. data/lib/sws/Core/components/CheckBoxList/CheckBoxList.api +13 -0
  86. data/lib/sws/Core/components/CheckBoxList/CheckBoxList.rb +54 -0
  87. data/lib/sws/Core/components/Conditional/Conditional.api +3 -0
  88. data/lib/sws/Core/components/Conditional/Conditional.html +1 -0
  89. data/lib/sws/Core/components/Conditional/Conditional.rb +39 -0
  90. data/lib/sws/Core/components/Conditional/Conditional.sws +2 -0
  91. data/lib/sws/Core/components/Content/Content.rb +18 -0
  92. data/lib/sws/Core/components/ExceptionPage/ExceptionPage.html +13 -0
  93. data/lib/sws/Core/components/ExceptionPage/ExceptionPage.rb +18 -0
  94. data/lib/sws/Core/components/ExceptionPage/ExceptionPage.sws +16 -0
  95. data/lib/sws/Core/components/FileUpload/FileUpload.api +16 -0
  96. data/lib/sws/Core/components/FileUpload/FileUpload.rb +62 -0
  97. data/lib/sws/Core/components/Form/Form.api +9 -0
  98. data/lib/sws/Core/components/Form/Form.html +3 -0
  99. data/lib/sws/Core/components/Form/Form.rb +55 -0
  100. data/lib/sws/Core/components/Form/Form.sws +12 -0
  101. data/lib/sws/Core/components/GenericContainer/GenericContainer.api +10 -0
  102. data/lib/sws/Core/components/GenericContainer/GenericContainer.html +1 -0
  103. data/lib/sws/Core/components/GenericContainer/GenericContainer.rb +39 -0
  104. data/lib/sws/Core/components/GenericContainer/GenericContainer.sws +12 -0
  105. data/lib/sws/Core/components/GenericElement/GenericElement.api +10 -0
  106. data/lib/sws/Core/components/GenericElement/GenericElement.rb +34 -0
  107. data/lib/sws/Core/components/HiddenField/HiddenField.api +7 -0
  108. data/lib/sws/Core/components/HiddenField/HiddenField.rb +37 -0
  109. data/lib/sws/Core/components/Hyperlink/Hyperlink.api +13 -0
  110. data/lib/sws/Core/components/Hyperlink/Hyperlink.html +1 -0
  111. data/lib/sws/Core/components/Hyperlink/Hyperlink.rb +102 -0
  112. data/lib/sws/Core/components/Hyperlink/Hyperlink.sws +12 -0
  113. data/lib/sws/Core/components/Image/Image.api +11 -0
  114. data/lib/sws/Core/components/Image/Image.rb +49 -0
  115. data/lib/sws/Core/components/ImageButton/ImageButton.api +16 -0
  116. data/lib/sws/Core/components/ImageButton/ImageButton.rb +76 -0
  117. data/lib/sws/Core/components/Link/Link.api +11 -0
  118. data/lib/sws/Core/components/Link/Link.rb +39 -0
  119. data/lib/sws/Core/components/PasswordField/PasswordField.api +7 -0
  120. data/lib/sws/Core/components/PasswordField/PasswordField.rb +41 -0
  121. data/lib/sws/Core/components/RadioButton/RadioButton.api +7 -0
  122. data/lib/sws/Core/components/RadioButton/RadioButton.rb +44 -0
  123. data/lib/sws/Core/components/RadioButtonList/RadioButtonList.api +20 -0
  124. data/lib/sws/Core/components/RadioButtonList/RadioButtonList.rb +76 -0
  125. data/lib/sws/Core/components/Repetition/Repetition.api +10 -0
  126. data/lib/sws/Core/components/Repetition/Repetition.html +1 -0
  127. data/lib/sws/Core/components/Repetition/Repetition.rb +137 -0
  128. data/lib/sws/Core/components/Repetition/Repetition.sws +2 -0
  129. data/lib/sws/Core/components/ResetButton/ResetButton.api +5 -0
  130. data/lib/sws/Core/components/ResetButton/ResetButton.rb +28 -0
  131. data/lib/sws/Core/components/Select/Select.api +24 -0
  132. data/lib/sws/Core/components/Select/Select.rb +57 -0
  133. data/lib/sws/Core/components/String/String.api +5 -0
  134. data/lib/sws/Core/components/String/String.rb +28 -0
  135. data/lib/sws/Core/components/SubmitButton/SubmitButton.api +6 -0
  136. data/lib/sws/Core/components/SubmitButton/SubmitButton.rb +53 -0
  137. data/lib/sws/Core/components/TextArea/TextArea.api +9 -0
  138. data/lib/sws/Core/components/TextArea/TextArea.rb +28 -0
  139. data/lib/sws/Core/components/TextField/TextField.api +9 -0
  140. data/lib/sws/Core/components/TextField/TextField.rb +46 -0
  141. data/lib/sws/Core/framework.yaml +25 -0
  142. data/lib/sws/JSComponents/components/JSMenu/JSMenu.api +5 -0
  143. data/lib/sws/JSComponents/components/JSMenu/JSMenu.html +58 -0
  144. data/lib/sws/JSComponents/components/JSMenu/JSMenu.rb +34 -0
  145. data/lib/sws/JSComponents/components/JSMenu/JSMenu.sws +37 -0
  146. data/lib/sws/JSComponents/framework.yaml +3 -0
  147. data/lib/sws/adaptor.rb +334 -0
  148. data/lib/sws/application.rb +604 -0
  149. data/lib/sws/component.rb +656 -0
  150. data/lib/sws/cookie.rb +27 -0
  151. data/lib/sws/direct_action.rb +38 -0
  152. data/lib/sws/extensions.rb +49 -0
  153. data/lib/sws/parsers.rb +374 -0
  154. data/lib/sws/request.rb +308 -0
  155. data/lib/sws/response.rb +70 -0
  156. data/lib/sws/session.rb +195 -0
  157. data/lib/sws/slot.rb +198 -0
  158. metadata +263 -0
@@ -0,0 +1,37 @@
1
+ contento:
2
+ _class: SWS::Content
3
+
4
+ menu_id_string:
5
+ _class: SWS::String
6
+ value: menu_id
7
+
8
+ menu_link:
9
+ _class: SWS::Hyperlink
10
+ href: "'#'"
11
+ other_tag_string: menu_link_js_events
12
+
13
+ menu_div:
14
+ _class: SWS::GenericContainer
15
+ tag_name: "'div'"
16
+ id: menu_div_id
17
+ other_tag_string: "'style=\"visibility: hidden; position:absolute; z-index:1; left:0; top:0\"'"
18
+
19
+ menu_table:
20
+ _class: SWS::GenericContainer
21
+ tag_name: "'table'"
22
+ class: menu_class
23
+
24
+ actions_repetition:
25
+ _class: SWS::Repetition
26
+ list: actions
27
+ item: action_cursor
28
+
29
+ action_link:
30
+ _class: SWS::Hyperlink
31
+ action: action_for_cursor
32
+ other_tag_string: "'onmousedown=\"document.onmousedown=null;\"'"
33
+
34
+ action_string:
35
+ _class: SWS::String
36
+ value: name_for_cursor
37
+
@@ -0,0 +1,3 @@
1
+ components:
2
+ JSComponents::JSMenu: components/JSMenu
3
+
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'socket'
4
+
5
+ module SWS
6
+
7
+
8
+ # Adaptor is a part of application that listens for requests. Currently there
9
+ # are 3 available adaptors - Standalone, FastCGI and CGI (the last one is
10
+ # experimental). You can write your own adaptor - it should just inherit from
11
+ # Generic adaptor and implement run and each_request methods.
12
+ module Adaptor
13
+
14
+
15
+ # Base class for all adaptors
16
+ class Generic
17
+
18
+ # Header translation hash - defines how the env variables are named in request.headers
19
+ # TODO: add missing headers
20
+ HEADER_TRANSLATION = {
21
+ "HTTP_USER_AGENT" => "user-agent",
22
+ "HTTP_HOST" => "host",
23
+ "HTTP_COOKIE" => "cookie",
24
+ "CONTENT_LENGTH" => "content-length",
25
+ "CONTENT_TYPE" => "content-type",
26
+ "REMOTE_ADDR" => "remote-address"
27
+ }
28
+
29
+ # Base path of the adaptor
30
+ attr_reader :base_path
31
+
32
+ def initialize ()
33
+ raise "Adaptor::Generic cannot be instantiated - override and don't call super"
34
+ end
35
+
36
+
37
+ # Start the adaptor. Custom adaptors should override this method and
38
+ # shouldn't call super
39
+ def run ()
40
+ raise "Override and do not call super for Adaptor.run"
41
+ end
42
+
43
+
44
+ # Main adaptor method - invokes block for each received request. Custom
45
+ # adaptors should override this method and shouldn't call super
46
+ def each_request ()
47
+ raise "Override and do not call super for Adaptor.each_request"
48
+ end
49
+
50
+ end
51
+
52
+
53
+ # Adaptor listening on a TCPport
54
+ class Standalone < Generic
55
+
56
+ attr_accessor :port
57
+
58
+ # Creates new adaptor listening on a given port
59
+ def initialize ( port = 1234 )
60
+
61
+ @port = port
62
+ #Always "/" for this class, as the application listens on its own port
63
+ @base_path = "/"
64
+
65
+ end
66
+
67
+
68
+ # Start listening for request
69
+ def run ()
70
+ @server = TCPServer.new( @port )
71
+ end
72
+
73
+
74
+ # Main adaptor method - invokes block for each received request
75
+ def each_request ()
76
+
77
+ while ( socket = @server.accept() )
78
+
79
+ request = create_request( socket )
80
+ response = yield( request )
81
+ begin
82
+ socket.print( "#{response.http_version} #{response.status}\r\n" )
83
+ socket.print( response.to_s )
84
+ socket.close()
85
+ rescue => exception
86
+ $log_sws_request.warn( "Exception #{exception.class} when writing response: #{exception.message}" )
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+
94
+ private
95
+
96
+ # Gets the request data from socket and creates the Request object
97
+ def create_request ( socket )
98
+
99
+ lines = Array.new()
100
+ request_method,whole_path,http_version = socket.gets().split( /\s+/ )
101
+ $log_sws_request.debug( "New request: #{whole_path}" )
102
+ path,query_string = whole_path.split( /\?/,2 )
103
+ request = Request.new( request_method, path, query_string, http_version )
104
+ # the "sub" is needed to get rid of IPv6 prefix
105
+ # TODO: make it more elegant and handle IPv6 adresses
106
+ request.headers["remote-address"] = socket.peeraddr[3].sub( /^.*:/,"" )
107
+ begin
108
+ line = socket.gets()
109
+ # Read all header lines
110
+ lines << line unless ( line == "\r\n" )
111
+ end while ( line != "\r\n" )
112
+ lines.each do |line|
113
+ key,value = line.strip().split( /:\s*/ )
114
+ key.downcase!()
115
+ request.headers[key] = value
116
+ $log_sws_request.debug( "Key: #{key}, value: #{value}\n" )
117
+ # read request content
118
+ if ( key == "content-length" )
119
+ # Headers has been read, so there is only the content itself left
120
+ request.content = socket.read( value.to_i )
121
+ end
122
+ end
123
+ request.process_headers()
124
+ request.process_content() if ( request.has_content? )
125
+ return request
126
+
127
+ end
128
+
129
+ end
130
+
131
+
132
+ # FastCGI adaptor. Note that at the moment it will only run properly if you
133
+ # start only one instace of the application. It SHOULD work ok if you run
134
+ # multiple instances and use session affinity patch for mod_fastcgi, but I
135
+ # didn't test that.
136
+ class FastCGI < Generic
137
+
138
+ def initialize
139
+
140
+ #The 'require' clause has been placed within initialize to avoid
141
+ #exception if the FastCGI adaptor is not used and fcgi.rb is missing
142
+ require 'fcgi'
143
+
144
+ #Base_path will be initialized on first request, because it is not known before
145
+ end
146
+
147
+
148
+ # Run for FastCGI adaptor is in fact performed in each_request, so this
149
+ # method is empty
150
+ def run
151
+
152
+ end
153
+
154
+
155
+ # Main adaptor method - invokes block for each received request
156
+ def each_request ()
157
+
158
+ FCGI.each_request do |fcgi_request|
159
+ #This is a FCGI request, not a SWS one - we need to transform it
160
+ sws_request = create_request( fcgi_request )
161
+
162
+ # Base path can only be deducted after first request
163
+ unless ( @base_path )
164
+ @base_path = fcgi_request.env["SCRIPT_NAME"] + "/"
165
+ end
166
+
167
+ response = yield( sws_request )
168
+ fcgi_request.out.print( "Status: #{response.status}\r\n" )
169
+ fcgi_request.out.print( response.to_s )
170
+ fcgi_request.finish
171
+ end
172
+
173
+ end
174
+
175
+
176
+ # Creates SWS::Request object using FastCGI request object
177
+ def create_request ( fcgi_request )
178
+
179
+ request_method = fcgi_request.env["REQUEST_METHOD"]
180
+ path = fcgi_request.env["PATH_INFO"] || "/"
181
+ # This may be invalid - but it isn't propably important
182
+ http_version = fcgi_request.env["SERVER_PROTOCOL"]
183
+ query_string = fcgi_request.env["QUERY_STRING"]
184
+ # We wan't the same behaviour as Standalone adaptor has
185
+ if query_string == "" then
186
+ query_string = nil
187
+ end
188
+ request = Request.new( request_method, path, query_string, http_version )
189
+
190
+ HEADER_TRANSLATION.each do |original_header,request_header|
191
+ request.headers[request_header] = fcgi_request.env[original_header]
192
+ end
193
+
194
+ content_length = fcgi_request.env["CONTENT_LENGTH"]
195
+ if ( content_length )
196
+ request.content = fcgi_request.in.read( content_length.to_i )
197
+ end
198
+
199
+ fcgi_request.env.each do |key,value|
200
+ unless HEADER_TRANSLATION.has_key?( key )
201
+ request.headers[key.downcase] = value
202
+ end
203
+ end
204
+ request.process_headers()
205
+ request.process_content() if ( request.has_content? )
206
+ return request
207
+
208
+ end
209
+
210
+ end
211
+
212
+
213
+ # CGI Adaptor. CGI scripts are not persitant, so I had to use some ugly
214
+ # hacks (including drb) to make it work transparently for the user (and
215
+ # developer). It has a serious bug - if the response is bigger than 8kB
216
+ # it is not returned to the client correctly. The use of this adaptor
217
+ # is discouraged and it may be removed in future versions.
218
+ class CGI < Generic
219
+
220
+
221
+ def initialize ( drb_port = 2345 )
222
+
223
+ # 'Require' included here, so that DRb is not loaded if this adaptor is
224
+ # not used
225
+ require 'drb'
226
+ @drb_port = drb_port
227
+
228
+ end
229
+
230
+
231
+ # Setup DRb server. Remember the DRBObject is the adaptor itself.
232
+ def run ()
233
+
234
+ request = create_request()
235
+
236
+ # Base_path will be set on every request. This doesn't matter though,
237
+ # because on the remote adaptor it will be set only once
238
+ @base_path = ENV["SCRIPT_NAME"] + "/"
239
+
240
+ begin
241
+
242
+ adaptor = DRb::DRbObject.new_with_uri( "druby://localhost:#{@drb_port}" )
243
+ # Try to use the remote Adaptor object
244
+ adaptor.handle_request( request )
245
+
246
+ rescue DRb::DRbConnError
247
+
248
+ # Attempt to retrieve remote Adaptor object failed - need to start the DRb server
249
+
250
+ # Probably not necessary
251
+ trap( "SIGCLD", "IGNORE" )
252
+
253
+ # Terminate parent and run child in daemon mode
254
+ if ( fork() ) then exit(0) end
255
+ DRb.start_service( "druby://localhost:#{@drb_port}", self )
256
+ Process.setsid()
257
+
258
+ @each_request_mutex = Mutex.new()
259
+ @each_request_mutex.lock()
260
+ @handle_request_mutex = Mutex.new()
261
+
262
+ handle_request( request )
263
+
264
+ end
265
+
266
+ end
267
+
268
+
269
+ # Main adaptor methods - invokes block for each received request
270
+ def each_request ()
271
+
272
+ loop do
273
+
274
+ @each_request_mutex.lock()
275
+ response = yield( @current_request )
276
+ # print( "Status: 200 OK\n" )
277
+ # print( "Content-type: text/html;charset=iso-8859-2\n")
278
+ # print( "Pragma: no-cache\n" )
279
+ # print( "Set-Cookie: session=6aa83079b309dd51d2dee0b1250719ca; path=/\n" )
280
+ # print( "\n")
281
+ print( "Status: #{response.status}\r\n" )
282
+ $stderr.puts( "B1" )
283
+ $stderr.puts( "BEfore Response: #{response.to_s}" )
284
+ print( response.to_s )
285
+ print( "\r\n" )
286
+ $stderr.puts( "After Response" )
287
+ @handle_request_mutex.unlock()
288
+
289
+ end
290
+
291
+ end
292
+
293
+
294
+ # Creates SWS::Request object from ENV variables
295
+ def create_request ()
296
+
297
+ request_method = ENV["REQUEST_METHOD"]
298
+ path = ENV["PATH_INFO"] || "/"
299
+ $stderr.puts( "Path: #{path}" )
300
+ # This may be invalid - but it isn't propably important
301
+ http_version = ENV["SERVER_PROTOCOL"]
302
+ request = Request.new( request_method, path, http_version )
303
+
304
+ HEADER_TRANSLATION.each do |env_variable,header_name|
305
+ request.headers[header_name] = ENV[env_variable]
306
+ end
307
+
308
+ content_length = ENV["CONTENT_LENGTH"]
309
+ if ( content_length )
310
+ request.content = $stdin.read( content_length.to_i )
311
+ end
312
+
313
+ request.process_headers()
314
+ request.process_content() if ( request.has_content? )
315
+ return request
316
+
317
+ end
318
+
319
+
320
+ protected
321
+ def handle_request( request )
322
+
323
+ @handle_request_mutex.lock()
324
+ @current_request = request
325
+ @each_request_mutex.unlock()
326
+
327
+ end
328
+
329
+
330
+ end
331
+
332
+ end
333
+
334
+ end
@@ -0,0 +1,604 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module SWS
4
+
5
+ APP_CONFIG_FILE = "application.yaml"
6
+ FRAMEWORK_CONFIG_FILE = "framework.yaml"
7
+
8
+ CONFIG_COMPONENTS = "components"
9
+ CONFIG_RESOURCES = "resources"
10
+ CONFIG_FRAMEWORKS = "frameworks"
11
+ CONFIG_LOAD_PATHS = "load_paths"
12
+
13
+ # Main class of each SWS application. Can be accessed as a singleton object.
14
+ # Contains global data (session independent) and perform fundamental
15
+ # request/response/exception handling. Defines request handlers.
16
+
17
+ class Application
18
+
19
+
20
+ # Singleton instance of an application
21
+ @@instance = nil
22
+
23
+ # Default encoding for components
24
+ attr_accessor :default_encoding
25
+
26
+ # Adaptor object - determines the type of interface the application uses
27
+ # (eg. Adaptor, FastCGIAdaptor etc)
28
+ attr_reader :adaptor
29
+
30
+ # Handlers for different types of requests.
31
+ # There are following types of requests:
32
+ # - component request: URL /cp/11324134 - request for existing component registered
33
+ # in @pages hash - the number is component id, 'cp' - request handler key
34
+ # - page by name request: URL /pn/132443243/pageName - request for component
35
+ # by its name - 'pn' is request handler key, number is _previous_ component id
36
+ # (it is necessary so that one cannot access page directly by just writing
37
+ # eg. /pn/43532322/pageName)
38
+ # - direct action: URL /da/className/actionName - like direct action in WO.
39
+ # Again, 'da' is request handler key, actionName is self-descripting
40
+ # - resource request: URL /rs/frameworkName/resourceName - request for
41
+ # application resource, eg. image or CSS file.
42
+ # You can also provide custom request handlers - just add them to
43
+ # @request_handlers hash.
44
+ # Key - request handler key (eg. 'pn', 'da'), value - method handling this
45
+ # type of request.
46
+ attr_reader :request_handlers
47
+
48
+ # Request handler key for component requests
49
+ attr_reader :component_request_handler_key
50
+
51
+ # Request handler key for page by name requests
52
+ attr_reader :page_name_request_handler_key
53
+
54
+ # Request handler key for direct action requests
55
+ attr_reader :direct_action_request_handler_key
56
+
57
+ # Request handler key for resource requests
58
+ attr_reader :resource_request_handler_key
59
+
60
+ # Paths in which all files of components will be searched for
61
+ attr_reader :component_paths
62
+
63
+ # Paths in which resources will be searched for
64
+ attr_reader :resource_paths
65
+
66
+ # Name of the subclass of Component that will be used as default for serving
67
+ # requests (eg. for default DirectAction or "/"). Defaults to "Main"
68
+ attr_reader :default_component_class_name
69
+
70
+ # Name of the subclass of DirectAction that will be used to serve
71
+ # DirectAction requests with no class specified. Defaults to DirectAction
72
+ attr_reader :default_direct_action_class
73
+
74
+ # Name of the subclass of Session that will be used for storing sessions.
75
+ # Defaults to Session
76
+ attr_reader :session_class
77
+
78
+ # Component class used to generate exception page
79
+ attr_reader :exception_component
80
+
81
+ # Creates new singleton Application object
82
+ def initialize ()
83
+
84
+ if ( @@instance != nil )
85
+ raise "Cannot create Application instance if one exists!"
86
+ else
87
+ @@instance = self
88
+ end
89
+
90
+ @request_count = 0
91
+ @frameworks = Hash.new
92
+
93
+ load_config_file()
94
+
95
+ setup_request_handlers()
96
+ setup_component_cache()
97
+ setup_session_cleaner()
98
+
99
+ end
100
+
101
+
102
+ private
103
+
104
+ # Reads the configuration from YAML file
105
+ def load_config_file()
106
+
107
+ @config = YAML::load( File.open( APP_CONFIG_FILE ) )
108
+
109
+ # Adaptor to use
110
+ @adaptor = @config["adaptor_class"] ?
111
+ SWS.get_class( @config["adaptor_class"] ).new : Adaptor::Standalone.new
112
+ # Class used for session objects
113
+ @session_class = @config["session_class"] ?
114
+ SWS.get_class( @config["session_class"] ) : Session
115
+ # Class used for direct actions (if no class explicitly specified)
116
+ @default_direct_action_class = @config["default_direct_action_class"] ?
117
+ SWS.get_class( @config["default_direct_action_class"] ) : DirectAction
118
+ # Name of component returned for new sessions
119
+ @default_component_class_name = @config["default_component_class"] || "Main"
120
+ # Default component encoding
121
+ @default_encoding = @config["default_encoding"] || "iso-8859-1"
122
+ # Name of component used to create exception pages
123
+ @exception_component = @config["exception_component_class"] || "SWS::ExceptionPage"
124
+
125
+ # Max number of sessions
126
+ @max_sessions = @config["max_sessions"] || 100
127
+ # Man number of top-level components per session
128
+ @max_components_per_session = @config["max_components_per_session"] || 5
129
+ # Sessions' inactivity time (in seconds) after which it will be swept out
130
+ @session_timeout = @config["session_timeout"] || 6*3600
131
+ # This time (in seconds) specifies how often session cleaning will be performed
132
+ @session_cleaner_interval = @config["session_cleaner_interval"] || 600
133
+ # Should the application refuse new sessions or kill Least Recently Used
134
+ # when limit reached?
135
+ @refuse_sessions = @config["refuse_sessions"] || false
136
+
137
+ @config[CONFIG_FRAMEWORKS].each_pair { |name,location| load_framework( name, location ) }
138
+
139
+ end
140
+
141
+
142
+ # Loads a framework
143
+ def load_framework( name, location )
144
+
145
+
146
+ config_file = find_framework_config_file( location )
147
+ unless ( config_file )
148
+ raise( "Cannot find framework #{name} in location #{location}" )
149
+ end
150
+
151
+ config = YAML::load( File.open( config_file ) )
152
+ path = File.dirname( config_file )
153
+ components = config[CONFIG_COMPONENTS] || Hash.new
154
+ resources = config[CONFIG_RESOURCES] || Hash.new
155
+ load_paths = config[CONFIG_LOAD_PATHS] || Array.new
156
+
157
+ @frameworks[name] = Framework.new( path,components,resources )
158
+
159
+ $LOAD_PATH.push( *load_paths )
160
+
161
+ end
162
+
163
+
164
+ # Searches for framework config file. Frameworks with location starting with
165
+ # "SYSTEM" are searched in standard locations ($LOAD_PATH/sws) - otherwise
166
+ # the location is relative to the application directory
167
+ # TODO: reconsider what should be added with respect to RubyGems
168
+ def find_framework_config_file( location )
169
+
170
+ if( location =~ /^SYSTEM/ )
171
+
172
+ loc = location.sub( /^SYSTEM/,"" )
173
+
174
+ $LOAD_PATH.each do |path|
175
+ config_file = File.join( path, loc, FRAMEWORK_CONFIG_FILE )
176
+ if ( File.exists?( config_file ) )
177
+ return config_file
178
+ end
179
+ end
180
+
181
+ else
182
+
183
+ config_file = File.join( location, FRAMEWORK_CONFIG_FILE )
184
+ if ( File.exists?( config_file ) )
185
+ return config_file
186
+ end
187
+
188
+ end
189
+
190
+ return nil
191
+
192
+ end
193
+
194
+ # Sets the request handlers up
195
+ def setup_request_handlers ()
196
+
197
+ #default request handlers' keys
198
+ @component_request_handler_key = "cp"
199
+ @page_name_request_handler_key = "pn"
200
+ @direct_action_request_handler_key = "da"
201
+ @resource_request_handler_key = "res"
202
+
203
+ #register default request handlers
204
+ @request_handlers = Hash.new()
205
+ @request_handlers [@component_request_handler_key] = :handle_component_request
206
+ @request_handlers [@page_name_request_handler_key] = :handle_page_name_request
207
+ @request_handlers [@direct_action_request_handler_key] = :handle_direct_action_request
208
+ @request_handlers [@resource_request_handler_key] = :handle_resource_request
209
+ #all other defaults to component requests
210
+ @request_handlers.default = :handle_component_request
211
+
212
+ end
213
+
214
+
215
+ def setup_component_cache
216
+ ComponentCache.max_components_per_session = @max_components_per_session
217
+ end
218
+
219
+ def setup_session_cleaner
220
+
221
+ @session_cleaner_mutex = Mutex.new
222
+ @session_cleaner = SessionCleaner.new( @session_class.sessions, @session_cleaner_mutex, @session_timeout, @session_cleaner_interval )
223
+
224
+ end
225
+
226
+
227
+ public
228
+
229
+ # Returns the singleton instance of the application
230
+ def Application.instance ()
231
+ return @@instance
232
+ end
233
+
234
+
235
+ # Starts the application (starts the adaptor and goes into #request_loop)
236
+ def run ()
237
+
238
+ @adaptor.run()
239
+ request_loop()
240
+
241
+ end
242
+
243
+
244
+ # Main request loop of the application. Reads Request objects from the
245
+ # adaptor and passes them to #handle_request method
246
+ def request_loop ()
247
+
248
+ @adaptor.each_request do |request|
249
+
250
+ @request_count += 1
251
+
252
+ # The response has to be returned from the block - handle request returns one
253
+ response = nil
254
+ @session_cleaner_mutex.synchronize {
255
+ response = handle_request( request )
256
+ }
257
+ response
258
+
259
+ end
260
+
261
+ end
262
+
263
+
264
+ # Exception handling method. If you want to customize error handling, just
265
+ # override this one.
266
+ def handle_exception ( request,exception )
267
+
268
+ $log_sws_request.warn( "Exception in application loop: #{exception}" )
269
+ $log_sws_request.warn( "Backtrace: #{exception.backtrace.join("\n")}" )
270
+
271
+ begin
272
+
273
+ exception_page = Component.create( @exception_component, request )
274
+ exception_page.exception = exception
275
+ component,response = exception_page.process_request( request )
276
+
277
+ rescue Exception => exception #just in case there is an error in SWS itself
278
+
279
+ $log_sws_component.warn( "Exception in exception component #{@exception_component}: #{exception}" )
280
+ $log_sws_component.warn( "Backtrace: #{exception.backtrace.join("\n")}" )
281
+
282
+ response = Response.new( request,"500" )
283
+ response.cookies << request.session.to_cookie
284
+ response.headers["Content-type"] = "text/html;charset=#{@default_encoding}"
285
+ response.headers["Pragma"] = "no-cache"
286
+ response.headers["Expires"] = "0"
287
+ response.headers["Cache-control"] = "private, no-cache, no-store, must-revalidate, max-age = 0"
288
+
289
+ response << "<HTML><BODY><H1>Exception raised!</H1>\n"
290
+ response << "<H3>#{exception.class}:#{exception}</H3>\n"
291
+ response << "Backtrace: <BR> #{exception.backtrace.join("<BR>\n")}"
292
+ response << "</BODY></HTML>\n"
293
+
294
+ end
295
+
296
+ return response
297
+
298
+ end
299
+
300
+
301
+ def handle_too_far_backtrack ( request )
302
+
303
+ # TODO: not sure about the status
304
+ response = Response.new( request,"200 SWS" )
305
+ response.cookies << request.session.to_cookie
306
+ response.headers["Content-type"] = "text/html;charset=#{@default_encoding}"
307
+ response.headers["Pragma"] = "no-cache"
308
+ response.headers["Expires"] = "0"
309
+ response.headers["Cache-control"] = "private, no-cache, no-store, must-revalidate, max-age = 0"
310
+
311
+ # TODO: allow the user to enter the application again
312
+ response << "<HTML><BODY><H1>You backtracked too far</H1>\n"
313
+ response << "</BODY></HTML>\n"
314
+ return nil,response
315
+
316
+ end
317
+
318
+ # Called when new session is to be refused. Default implementation return a
319
+ # simplistic info page. Note that this method probably shouldn't return a
320
+ # component, as a component may require session to work properly.
321
+ def handle_refuse_session ( request )
322
+
323
+ response = Response.new( request ,"503" )
324
+ response.headers["Content-type"] = "text/html;charset=#{@default_encoding}"
325
+ response.headers["Pragma"] = "no-cache"
326
+ response.headers["Expires"] = "0"
327
+ response.headers["Cache-control"] = "private, no-cache, no-store, must-revalidate, max-age = 0"
328
+
329
+ response << "<HTML><BODY><H1>Session refused!</H1>\n"
330
+ response << "<H3>Session refused due to application overload. Please try again later.</H3>"
331
+ response << "</BODY></HTML>\n"
332
+
333
+ return response
334
+
335
+ end
336
+
337
+
338
+ # Takes the request and calls one of request_handlers basing on request
339
+ # handler key contained in URL. Also performs session retrieval/creation.
340
+ def handle_request ( request )
341
+
342
+ # hack for requests for favicon if it doesn't exist
343
+ # TODO: it blocks ALL favicon.ico requests, but should block only if
344
+ # favicon.ico is not provided
345
+ # TODO: or we should omit this issue after implementation of
346
+ # handling more than one current component
347
+ if ( request.query_string =~ /favicon.ico$/ || request.path =~ /favicon.ico$/ )
348
+ return Response.new( request, "404 Not Found" )
349
+ end
350
+
351
+ session = request.session
352
+ if ( session == nil )
353
+
354
+ if ( @session_class.sessions.size < @max_sessions )
355
+
356
+ session = @session_class.new
357
+ request.session = session
358
+
359
+ else # Too many sessions
360
+
361
+ if ( @refuse_sessions )
362
+ return handle_refuse_session( request )
363
+ else
364
+ # Delete Least Recently Used session
365
+ session_to_delete = @session_class.sessions.values.inject do |session1,session2|
366
+ session1.last_access_time < session2.last_access_time ? session1 : session2
367
+ end
368
+
369
+ @session_class.delete_session( session_to_delete )
370
+ session = @session_class.new
371
+ request.session = session
372
+ end
373
+
374
+ end
375
+
376
+ end
377
+
378
+ session.last_access_time = Time.now
379
+
380
+ #strip leading /'s
381
+ path = request.path.sub( /^\/+/,"" )
382
+
383
+ #path =~ ^/key/url_content$
384
+ if ( md = /^([^\/]*)\/+(.*)$/.match( path ) )
385
+
386
+ request_handler_key = md[1]
387
+ #TODO: rename it: its just the url without request handler key
388
+ #I assume the query string is already stripped
389
+ url_content = md[2]
390
+ request_handler = method( @request_handlers[request_handler_key] )
391
+ else
392
+ #call default request handler
393
+ request_handler = method( @request_handlers[path] )
394
+ url_content = path
395
+ end
396
+
397
+ begin
398
+
399
+ component,response = request_handler.call( request,url_content )
400
+ # The resource request handler does not return component
401
+
402
+ # That means that we only want to send response without any component.
403
+ # Variable component is used to return response. Look at
404
+ # SWS::Component#process_request for more.
405
+ if( response == nil )
406
+ return component
407
+ end
408
+
409
+ if ( component )
410
+ request.session.add_to_cache( component )
411
+ end
412
+ return response
413
+
414
+ rescue Exception => exception
415
+
416
+ return handle_exception( request,exception )
417
+
418
+ end
419
+
420
+
421
+ end
422
+
423
+
424
+ # Handles component requests
425
+ def handle_component_request ( request,url_content )
426
+
427
+ # check if url_content =~ /^component_data(/action)
428
+ if ( md = /^([^\/]*)\/+(.*)$/.match( url_content ) )
429
+
430
+ component_data = md[1]
431
+ action_object_id = md[2].to_i
432
+
433
+ else #no "/" after component_id
434
+
435
+ component_data = url_content
436
+
437
+ end
438
+
439
+ component_id, request_number = component_data.split( /\./ ).collect { |el| el.to_i }
440
+
441
+ $log_sws_component.debug( "Request number: #{request_number}" )
442
+ $log_sws_component.debug( "Got component id: #{component_id}" )
443
+ component = request.session.get_from_cache( component_id )
444
+ $log_sws_component.debug( "Retrieved component id: #{component.object_id}" )
445
+ unless ( component && request_number )
446
+ if request.session.old?( component_id ) # Backtracked too far
447
+ return handle_too_far_backtrack( request )
448
+ else # new session or random URL - redirect to default component
449
+ # We don't care about any parameters
450
+ request.erase_content()
451
+ component = Component.create( @default_component_class_name,request )
452
+ request_number = nil
453
+ end
454
+ else
455
+
456
+ # TODO: make it possible to present a custom error page on backtrack (it
457
+ # would require holding ids of all old components in session)
458
+ if ( action_object_id )
459
+ #action_object_id contains id of object, whose action should be performed
460
+ #right now it can only be a Hyperlink or a Form
461
+ action_object = component.action_components[ action_object_id ]
462
+ if ( action_object )
463
+ # We don't call the action immediately - only mark it as enabled and
464
+ # it will be called during call_action phase of request-response loop
465
+ action_object.enable_action()
466
+ else
467
+ component = Component.create( @default_component_class_name,request )
468
+ end
469
+ end
470
+ end
471
+
472
+ $log_sws_component.debug( "Component id: #{component.object_id}, request_number #{request_number}" )
473
+ return component.process_request( request, request_number )
474
+
475
+ end
476
+
477
+
478
+ # Handles page name requests
479
+ def handle_page_name_request ( request,url_content )
480
+
481
+ if ( md = /^(-?\d+)\/(.+)/.match( url_content ) )
482
+
483
+ previous_component = request.session.get_from_cache( md[1].to_i )
484
+ unless ( previous_component )
485
+
486
+ request.erase_content()
487
+ component = Component.create( @default_component_class_name,request )
488
+
489
+ return component.process_request( request )
490
+
491
+ else
492
+
493
+ next_component_name = md[2]
494
+ component = Component.create( next_component_name, request, next_component_name)
495
+
496
+ return component.process_request( request )
497
+
498
+ end
499
+
500
+ else
501
+ raise( "Malformed url for pn type request" )
502
+ end
503
+
504
+ end
505
+
506
+
507
+ # Handles direct action requests
508
+ def handle_direct_action_request ( request,url_content )
509
+
510
+ class_name, method_name = url_content.split( /\/+/,2 )
511
+ if ( method_name )
512
+ klass = SWS.get_class( class_name + "Action" )
513
+ else
514
+ klass = @default_direct_action_class
515
+ method_name = class_name
516
+ end
517
+ unless ( klass && klass.ancestors.include?( DirectAction ) )
518
+ raise TypeError.new( "Direct action class name for url #{url_content} does not inherit from SWS::DirectAction" )
519
+ end
520
+
521
+ direct_action = klass.new( request )
522
+
523
+ new_component, response = direct_action.action_for_name( method_name )
524
+
525
+ return [new_component, response]
526
+
527
+ end
528
+
529
+
530
+ # Handles resource requests
531
+ def handle_resource_request ( request,url_content )
532
+
533
+ response = Response.new( request )
534
+
535
+ framework_name,resource_name = url_content.split( /\/+/,2 )
536
+ if ( framework_name == 'app' )
537
+ resource = @config[CONFIG_RESOURCES][resource_name]
538
+ else
539
+ framework = @frameworks[framework_name]
540
+ resource = framework.resources[resource_name]
541
+ end
542
+
543
+ unless ( resource )
544
+ raise( "Cannot find resource #{resource_name} in framework #{framework}" )
545
+ end
546
+
547
+ response.headers["Content-type"] = resource["mime-type"]
548
+ if ( framework )
549
+ path = File.join( framework.path, resource["filename"] )
550
+ else
551
+ path = resource["filename"]
552
+ end
553
+ unless( File.exists?( path ) )
554
+ raise( "File #{path} does not exist" )
555
+ end
556
+
557
+ file = File.open( path )
558
+ response << file.read
559
+ file.close()
560
+
561
+ return nil,response
562
+
563
+ end
564
+
565
+
566
+ # Returns ComponentFiles struct for given component name. Usually called
567
+ # only once per component class, so it is a good place to require its ruby
568
+ # file.
569
+ def get_component( component_name )
570
+
571
+ # Find component path
572
+ path = @config[CONFIG_COMPONENTS][component_name]
573
+
574
+ unless ( path )
575
+ framework = @frameworks.values.find { |framework| framework.components[component_name] }
576
+ if ( framework )
577
+ path = File.join( framework.path,framework.components[component_name] )
578
+ else
579
+ raise( "Component #{component_name} was not found in application or frameworks" )
580
+ end
581
+ end
582
+
583
+ # Retrieve component files. For components in nested namespaces only the
584
+ # last element of the name is used.
585
+ file = File.join( path, component_name.split( /::/ ).last )
586
+
587
+ if ( FileTest.file?( file+".rb" ) ) then ruby_file = file+".rb" end
588
+ if ( FileTest.file?( file+".sws" ) ) then sws_file = file+".sws" end
589
+ if ( FileTest.file?( file+".html" ) ) then html_file = file+".html" end
590
+ if ( FileTest.file?( file+".api" ) ) then api_file = file+".api" end
591
+
592
+ if ( ruby_file )
593
+ require( ruby_file )
594
+ klass = SWS.get_class( component_name )
595
+ return ComponentInfo.new( klass, sws_file, html_file, api_file )
596
+ end
597
+
598
+ raise "Cannot find component files for component #{component_name}"
599
+
600
+ end
601
+
602
+ end
603
+
604
+ end