sws 0.4

Sign up to get free protection for your applications and to get access to all the features.
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