nitro 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/AUTHORS +8 -0
  2. data/ChangeLog +1546 -0
  3. data/LICENCE +32 -0
  4. data/README +278 -0
  5. data/RELEASES +7 -0
  6. data/Rakefile +79 -0
  7. data/bin/cluster.rb +219 -0
  8. data/doc/architecture.txt +28 -0
  9. data/doc/bugs.txt +7 -0
  10. data/doc/css.txt +20 -0
  11. data/doc/ideas.txt +120 -0
  12. data/doc/pg.txt +47 -0
  13. data/doc/svn.txt +82 -0
  14. data/doc/todo.txt +30 -0
  15. data/etc/new-project.rb +18 -0
  16. data/examples/simple/README +15 -0
  17. data/examples/simple/app.rb +31 -0
  18. data/examples/simple/conf/apache.conf +100 -0
  19. data/examples/simple/conf/config.rb +89 -0
  20. data/examples/simple/conf/debug-config.rb +53 -0
  21. data/examples/simple/conf/live-config.rb +48 -0
  22. data/examples/simple/conf/overrides.rb +9 -0
  23. data/examples/simple/conf/requires.rb +51 -0
  24. data/examples/simple/ctl +32 -0
  25. data/examples/simple/env.rb +33 -0
  26. data/examples/simple/install.rb +12 -0
  27. data/examples/simple/lib/articles/entities.rb +35 -0
  28. data/examples/simple/lib/articles/lc-en.rb +36 -0
  29. data/examples/simple/lib/articles/methods.rb +55 -0
  30. data/examples/simple/lib/articles/part.rb +58 -0
  31. data/examples/simple/logs/access_log +2 -0
  32. data/examples/simple/logs/apache.log +3 -0
  33. data/examples/simple/logs/app.log +1 -0
  34. data/examples/simple/logs/events.log +1 -0
  35. data/examples/simple/root/add-article.sx +15 -0
  36. data/examples/simple/root/article-form.ss +20 -0
  37. data/examples/simple/root/comments-form.ss +16 -0
  38. data/examples/simple/root/comments.si +30 -0
  39. data/examples/simple/root/index.sx +44 -0
  40. data/examples/simple/root/shader/shader.xsl +100 -0
  41. data/examples/simple/root/shader/style.css +9 -0
  42. data/examples/simple/root/view-article.sx +30 -0
  43. data/examples/tiny/app.rb +30 -0
  44. data/examples/tiny/conf/apache.conf +100 -0
  45. data/examples/tiny/conf/config.rb +67 -0
  46. data/examples/tiny/conf/requires.rb +40 -0
  47. data/examples/tiny/ctl +31 -0
  48. data/examples/tiny/logs/access_log +9 -0
  49. data/examples/tiny/logs/apache.log +9 -0
  50. data/examples/tiny/root/index.sx +35 -0
  51. data/lib/n/app/cluster.rb +219 -0
  52. data/lib/n/app/cookie.rb +86 -0
  53. data/lib/n/app/filters/autologin.rb +50 -0
  54. data/lib/n/app/fragment.rb +67 -0
  55. data/lib/n/app/handlers.rb +120 -0
  56. data/lib/n/app/handlers/code-handler.rb +184 -0
  57. data/lib/n/app/handlers/page-handler.rb +612 -0
  58. data/lib/n/app/request-part.rb +59 -0
  59. data/lib/n/app/request.rb +653 -0
  60. data/lib/n/app/script.rb +398 -0
  61. data/lib/n/app/server.rb +53 -0
  62. data/lib/n/app/session.rb +224 -0
  63. data/lib/n/app/user.rb +47 -0
  64. data/lib/n/app/webrick-servlet.rb +213 -0
  65. data/lib/n/app/webrick.rb +70 -0
  66. data/lib/n/application.rb +187 -0
  67. data/lib/n/config.rb +31 -0
  68. data/lib/n/db.rb +217 -0
  69. data/lib/n/db/README +232 -0
  70. data/lib/n/db/connection.rb +369 -0
  71. data/lib/n/db/make-release.sh +26 -0
  72. data/lib/n/db/managed.rb +235 -0
  73. data/lib/n/db/mixins.rb +282 -0
  74. data/lib/n/db/mysql.rb +342 -0
  75. data/lib/n/db/psql.rb +378 -0
  76. data/lib/n/db/tools.rb +110 -0
  77. data/lib/n/db/utils.rb +99 -0
  78. data/lib/n/events.rb +118 -0
  79. data/lib/n/l10n.rb +22 -0
  80. data/lib/n/logger.rb +33 -0
  81. data/lib/n/macros.rb +53 -0
  82. data/lib/n/mixins.rb +46 -0
  83. data/lib/n/parts.rb +154 -0
  84. data/lib/n/properties.rb +194 -0
  85. data/lib/n/server.rb +61 -0
  86. data/lib/n/server/PLAYBACK.txt +8 -0
  87. data/lib/n/server/RESEARCH.txt +13 -0
  88. data/lib/n/server/filter.rb +77 -0
  89. data/lib/n/shaders.rb +167 -0
  90. data/lib/n/sitemap.rb +188 -0
  91. data/lib/n/std.rb +69 -0
  92. data/lib/n/sync/clc.rb +108 -0
  93. data/lib/n/sync/handler.rb +221 -0
  94. data/lib/n/sync/server.rb +170 -0
  95. data/lib/n/tools/README +11 -0
  96. data/lib/n/ui/date-select.rb +74 -0
  97. data/lib/n/ui/pager.rb +187 -0
  98. data/lib/n/ui/popup.rb +45 -0
  99. data/lib/n/ui/select.rb +41 -0
  100. data/lib/n/ui/tabs.rb +34 -0
  101. data/lib/n/utils/array.rb +92 -0
  102. data/lib/n/utils/cache.rb +144 -0
  103. data/lib/n/utils/gfx.rb +108 -0
  104. data/lib/n/utils/hash.rb +148 -0
  105. data/lib/n/utils/html.rb +147 -0
  106. data/lib/n/utils/http.rb +98 -0
  107. data/lib/n/utils/mail.rb +28 -0
  108. data/lib/n/utils/number.rb +31 -0
  109. data/lib/n/utils/pool.rb +66 -0
  110. data/lib/n/utils/string.rb +297 -0
  111. data/lib/n/utils/template.rb +38 -0
  112. data/lib/n/utils/time.rb +91 -0
  113. data/lib/n/utils/uri.rb +193 -0
  114. data/lib/xsl/base.xsl +205 -0
  115. data/lib/xsl/ce.xsl +30 -0
  116. data/lib/xsl/localization.xsl +23 -0
  117. data/lib/xsl/xforms.xsl +26 -0
  118. data/test/run.rb +95 -0
  119. metadata +187 -0
@@ -0,0 +1,50 @@
1
+ # = AutoLogin filter
2
+ #
3
+ # code:: gmosx
4
+ #
5
+ # (c) 2004 Navel, all rights reserved.
6
+ # $Id: autologin.rb 79 2004-10-18 16:38:19Z gmosx $
7
+
8
+ require "n/utils/string"
9
+ require "n/server/filter"
10
+
11
+ require_part "users"
12
+
13
+ module N; module App
14
+
15
+ # = AutoLoginFilter
16
+ #
17
+ # Automatically login a user with valid authentication cookie.
18
+ # Uses some n1 code at the moment.
19
+ #
20
+ class AutoLoginFilter < N::ServerFilter
21
+
22
+ def process(request)
23
+ if request.is_top?
24
+ # only try to autologin for top level requests
25
+ # there is NO need for the older SKIP_AUTOLOGIN session flag.
26
+ if (request.session.user.anonymous?) and
27
+ cookie = request.get_cookie($users_auth_cookie)
28
+
29
+ username, password_crypted = cookie.split(",")
30
+
31
+ if user = $db.get_by_name(username, N::User) and
32
+ password_crypted == user.password
33
+ if request.session.login(request, user)
34
+ # user logged in
35
+ else
36
+ # stale cookie, remove it
37
+ request.del_cookie($users_auth_cookie)
38
+ end
39
+ else
40
+ $log.warn "Unknown user or wrong password in auth-cookie: #{cookie} from IP: #{request.remote_addr}"
41
+ end
42
+ end
43
+ end
44
+
45
+ return @next_filter.process(request) if @next_filter
46
+ end
47
+
48
+ end
49
+
50
+ end; end # module
@@ -0,0 +1,67 @@
1
+ # = Fragment
2
+ #
3
+ # A Fragment is the output of a rendered script. Additional metadata
4
+ # such as lastmodified and expire times are stored to facilitate
5
+ # fragment processing (for example caching).
6
+ #
7
+ # === Design:
8
+ #
9
+ # Fragments are cached in the filesystem to allow for a cluster of
10
+ # server to access them. Benefits over the older memory cache scheme:
11
+ #
12
+ # - each fragment is processed once by one server
13
+ # - easier visualisation of the fragments.
14
+ # - can clear the cache for all server
15
+ # - can selectively invalidate one fragment!
16
+ # - less memory per server
17
+ # - can run background cron scripts over the fragments (compression)
18
+ #
19
+ # code:: gmosx
20
+ #
21
+ # (c) 2004 Navel, all rights reserved.
22
+ # $Id: fragment.rb 71 2004-10-18 10:50:22Z gmosx $
23
+
24
+ require "n/utils/cache"
25
+ require "n/mixins"
26
+
27
+ module N; module App
28
+
29
+ class Fragment
30
+ include N::Expirable
31
+ include N::LRUCache::Item
32
+
33
+ # precompiled flags for fragment key "customization"
34
+
35
+ ADMIN_FLAG = "-a"
36
+ OWNER_FLAG = "-o"
37
+ EDITOR_FLAG = "-e"
38
+ VIEWER_FLAG = "-f"
39
+ MY_FLAG = "-m"
40
+ ANONYMOUS_FLAG = "-n"
41
+
42
+ attr_accessor :body, :lm
43
+
44
+ # another method for invalidation, allows for more flexible
45
+ # invalidation strategies. also used by the legacy autoinvalidate
46
+ # code.
47
+ attr_accessor :expires
48
+
49
+ def initialize(body = "", lm = Time.now)
50
+ @body = body
51
+ @lm = lm
52
+ end
53
+
54
+ def expires_after!(ea = (60*60*24))
55
+ @expires = @lm + ea
56
+ end
57
+
58
+ def expired?
59
+ return true if @expires.nil? || (Time.now > @expires)
60
+ end
61
+
62
+ def to_str
63
+ return @body
64
+ end
65
+ end
66
+
67
+ end; end # module
@@ -0,0 +1,120 @@
1
+ # = Handlers
2
+ #
3
+ # code: gmosx
4
+ #
5
+ # (c) 2002-2003 Navel, all rights reserved.
6
+ # $Id: handlers.rb 71 2004-10-18 10:50:22Z gmosx $
7
+
8
+ require "n/utils/cache"
9
+ require "n/server/filter"
10
+
11
+ module N; module App
12
+
13
+ # = App server handlers handle requests to specific resources.
14
+ #
15
+ # === Design:
16
+ #
17
+ # Handlers are NOT singleton classes. This way we can assign one handler
18
+ # class to multiple resources, and keep statistics and metrics for
19
+ # each resource.
20
+ #
21
+ class Handler < N::ServerFilter
22
+ def process(request)
23
+ # nop
24
+
25
+ # walk the filter pipeline
26
+ @next_filter.process(request) if @next_filter
27
+ end
28
+
29
+ #---------------------------------------------------------------------
30
+ # Testing/Metrics support methods
31
+
32
+
33
+ end # class
34
+
35
+ # Handler Error.
36
+ # raise this if an error happens when handling a request
37
+
38
+ class HandlerError < StandardError; end
39
+
40
+ # = Script Handler
41
+ #
42
+ # Base handler for scripts.
43
+
44
+ class ScriptHandler < Handler
45
+
46
+ # cache the compiled page scripts to optimize future references.
47
+ # use a thread safe cache.
48
+ @@compiled_script_cache = N::SafeHash.new()
49
+
50
+ # dont allow 2 threads to compile the same script. In fact dont allow
51
+ # two threads to compile in parallel.
52
+ @@compile_sync = Sync.new
53
+
54
+ # Overload the path.
55
+ # FIXME: better name and much better documentation.
56
+ #
57
+ def overload_path(path)
58
+ path.gsub!(/\/\//, '/')
59
+
60
+ if ::File.exists?("#$root_dir/#{path}")
61
+ return path
62
+ else
63
+ $log.debug "OVERLOAD: '#{path}' -> 'p/#{path}'" if $DBG
64
+ return "p/#{path}"
65
+ end
66
+ end
67
+
68
+ # the compiler returns a singleton class customized for rendering the
69
+ # input script. The original idea was to define render() as a static
70
+ # method and return the class, but we will use a singleton object to
71
+ # keep custom data structures (sub-page-graph, metrics, etc)
72
+ #
73
+ # script_path is used as key in the Dynamic class creation and for
74
+ # caching
75
+ #
76
+ # === Design:
77
+ # we use __ for out too to avoid nasty collisions.
78
+ #
79
+ def compile_script(script)
80
+ compiled_script = nil
81
+
82
+ # dont allow 2 threads to compile the same script. In fact dont
83
+ # allow two threads to compile in parallel.
84
+ # gmosx: SOS this eval assigns the variable compiled_script!
85
+ @@compile_sync.synchronize {
86
+ eval(script)
87
+ }
88
+
89
+ return compiled_script
90
+ end
91
+
92
+ def compiled_script_cache
93
+ return @@compiled_script_cache
94
+ end
95
+
96
+ # Log a rendering error
97
+ #
98
+ def log_error(request, ex)
99
+ request.log_error "--------"
100
+ request.log_error "#{request.path}:"
101
+ request.log_error "#{ex.class}: #{ex}"
102
+ request.log_error ex.backtrace()
103
+ raise ScriptHandlerError.new(0, "error")
104
+ end
105
+
106
+ end
107
+
108
+ # Handler Error.
109
+ # raise this if an error happens when handling a request
110
+
111
+ class ScriptHandlerError < HandlerError
112
+ attr_reader :error_line
113
+
114
+ def initialize(error_line, message = nil)
115
+ @error_line, @message = error_line, message
116
+ end
117
+
118
+ end
119
+
120
+ end; end # module
@@ -0,0 +1,184 @@
1
+ # = Code handler (.sx scripts)
2
+ #
3
+ # code:
4
+ # George Moschovitis <gm@navel.gr>
5
+ #
6
+ # (c) 2004 Navel, all rights reserved.
7
+ # $Id: code-handler.rb 71 2004-10-18 10:50:22Z gmosx $
8
+
9
+ require "cgi"
10
+ require "singleton"
11
+ require "sync"
12
+
13
+ require "n/utils/cache"
14
+ require "n/utils/uri"
15
+ require "n/app/script"
16
+ require "n/app/fragment"
17
+ require "n/app/handlers"
18
+
19
+ module N; module App
20
+
21
+ # = CodeHandler
22
+ #
23
+ # web server handler that executes ruby code (logic)
24
+ # Caching is NOT SUPPORTED (and not needed anyway)
25
+ #
26
+ # TODO:
27
+ #
28
+ # probably extend PageHandler from CodeHandler and not vice versa.
29
+ #
30
+ class CodeHandler < ScriptHandler
31
+
32
+ # process is called ONLY for top level scripts.
33
+ #
34
+ # no need for synchronization, the page-script is thread safe.
35
+ # if you use thread-unsafe code in your script you are responsible
36
+ # for synchronization.
37
+ #
38
+ # TODO:
39
+ # add timing code here
40
+ #
41
+ def process(request)
42
+ # gmosx, FIXME: temporarily here, move somewhere ELSE!
43
+
44
+ request.locale = $lc_map[request.user.locale] || $lc_en
45
+ script = get_compiled_script(request.path)
46
+ fragment = eval_script(script, request)
47
+
48
+ # walk the filter pipeline
49
+ super
50
+
51
+ return fragment, script
52
+ end
53
+
54
+ # evaluate the request script in the context of the current request.
55
+ # returns the output of the script, ie the fragment body.
56
+ #
57
+ # === Design:
58
+ #
59
+ # I think that the script caching logic should be here, because it nicelly
60
+ # encapsulates transform and compile.
61
+ #
62
+ def eval_script(script, request)
63
+
64
+ $log.debug "Evaluating, #{request.path}" if $DBG
65
+
66
+ fragment = Fragment.new(:key)
67
+
68
+ # try to render the script, report errors.
69
+ begin
70
+ fragment.body = script.__render(request)
71
+ rescue N::ScriptExitException => see
72
+ # the script raised a ScripteExitException to force a premature
73
+ # end. Surpress this error!
74
+ rescue => ex
75
+ log_error(request, ex)
76
+ end
77
+
78
+ return fragment
79
+ end
80
+
81
+ # === Output:
82
+ # defcode: the code that customizes the base script class.
83
+ # pagecode: the code that renders the fragment.
84
+ #
85
+ # === REMARKS:
86
+ # - this method is evaluated in compile time, so we cannot pass
87
+ # or use the request/request pair. gmosx: NO, we do pass request,
88
+ # some data can and SHOULD be used to prepare the transform.
89
+ #
90
+ def transform_script(path, hash, shader = nil)
91
+
92
+ path = overload_path(path)
93
+
94
+ # load the page text from the script
95
+ pagecode = ""
96
+
97
+ pagecode = File.read("#$root_dir/#{path}")
98
+
99
+ # convert to ruby code
100
+ pagecode.gsub!(/<<</, "__out << %{")
101
+ pagecode.gsub!(/>>>/, "}")
102
+
103
+ script = %{
104
+ class CodeScript_#{hash} < CodeScript
105
+ def initialize(path)
106
+ super
107
+ @pagecode = <<-'PAGECODE'
108
+ #{pagecode}
109
+ PAGECODE
110
+ @defcode = nil
111
+ end
112
+ def __render(request)
113
+ __out = ""
114
+ lc = request.locale
115
+ #{pagecode}
116
+ return __out
117
+ end
118
+ end
119
+ return CodeScript_#{hash}.new("#{path}")
120
+ }
121
+
122
+ return script
123
+ end
124
+
125
+ # try to get the script from the cache. invalidates the compiled version and return nil
126
+ # if the script is modified since compile time. Also takes active shader and localization
127
+ # into account. If script is not compiled, transform and compile it.
128
+ #
129
+ # Output:
130
+ # the compiled script. Throws exception if the script does not exist.
131
+ #
132
+ def get_compiled_script(path, hash = nil)
133
+ compiled_script = nil
134
+ key = calc_script_key(path, hash)
135
+
136
+ # gmosx: $reload_scripts is typically set to true when debugging
137
+ # statically included files (.ss)
138
+ unless $reload_scripts
139
+
140
+ compiled_script = @@compiled_script_cache[key]
141
+
142
+ # gmosx: monitor scripts should be explicit!
143
+ if $srv_monitor_scripts and compiled_script and (File.mtime(compiled_script.path) > compiled_script.__create_time)
144
+ $log.debug "Script '#{path}' externaly modified, recompiling" if $DBG
145
+ compiled_script = nil
146
+ end
147
+
148
+ end
149
+
150
+ unless compiled_script
151
+ # the script is not cached, so load, transform and compile it!
152
+ $log.debug "Compiling script '#{key}'" if $DBG
153
+
154
+ script = transform_script(path, key)
155
+ compiled_script = compile_script(script)
156
+
157
+ @@compiled_script_cache[key] = compiled_script
158
+ end
159
+
160
+ return compiled_script
161
+ end
162
+
163
+ def calc_script_key(path, hash)
164
+ return "#{path}#{hash}".gsub(/[^\w]/, "")
165
+ end
166
+
167
+ end # CodeHandler
168
+
169
+ # = Codescript
170
+ #
171
+ # encapsulates the script defining Ruby Code to execute.
172
+ # effectively acts as a Generator.
173
+ #
174
+ # === Requirements:
175
+ #
176
+ # - the generated class should be a singleton, no need to stress
177
+ # the garbage collector.
178
+ # - the render method should be thread safe.
179
+ #
180
+ class CodeScript < Script
181
+
182
+ end # CodeScript
183
+
184
+ end; end # module
@@ -0,0 +1,612 @@
1
+ # = XML Page handler (.sx scripts)
2
+ #
3
+ # code:
4
+ # George Moschovitis <gm@navel.gr>
5
+ # Anastasios Koutoumanos <ak@navel.gr>
6
+ #
7
+ # (c) 2004 Navel, all rights reserved.
8
+ # $Id: page-handler.rb 89 2004-10-20 12:55:58Z gmosx $
9
+
10
+ require "cgi"
11
+ require "singleton"
12
+ require "sync"
13
+
14
+ require "n/utils/cache"
15
+ require "n/utils/uri"
16
+ require "n/app/script"
17
+ require "n/app/fragment"
18
+ require "n/app/handlers"
19
+
20
+ module N; module App
21
+
22
+ # = PageHandler
23
+ #
24
+ # web server handler that render xml pages (.sx scripts).
25
+ # This is the main handler of the Nitro Application Server.
26
+ #
27
+ # The handler evaluates the given script. The result of
28
+ # this evaluation is called a Fragment. The result of a top
29
+ # level script, ie a top level fragments is called a page.
30
+ #
31
+ # == Advantages over the original .rx format:
32
+ #
33
+ # - xml based
34
+ # - strips <!-- comments
35
+ # - allows multiline comments
36
+ # - allows pre-transformation with xlst (free transformation)
37
+ # - allows validation of xhtml
38
+ # - allows syntax highlighting
39
+ #
40
+ # == TODO:
41
+ #
42
+ # - move shader selection in a script method, that can
43
+ # be overriden (user specific shaders).
44
+ # - compress the output some more, every byte
45
+ # counts (bandwidth == money)
46
+ # - use something like FormValidator to validate parameters
47
+ # for sub-fragments.
48
+ #
49
+ # === FIXME:
50
+ # - correct encoding of fragment hash
51
+ # (query string, shader, user etc)
52
+ #
53
+ # === Design:
54
+ #
55
+ # break the rendering process in sub methods to make testable
56
+ # and verifiable (and easier to read).
57
+ #
58
+ # Statically included scripts (.ss) SHOULD be valid xml files (even
59
+ # though they are not required) to be automatically verifiable and
60
+ # compatible with editors. The xml prologue (<?xml / <root>) is removed.
61
+ #
62
+ # Be carefull about statically included files scope issues. This is the
63
+ # reason why we do not use statically included files everywhere.
64
+ #
65
+ # === WARNING:
66
+ #
67
+ # no need to lock cache io, we use a safe cache!
68
+ #
69
+ class PageHandler < ScriptHandler
70
+
71
+ # process is called ONLY for top level scripts.
72
+ #
73
+ # no need for synchronization, the page-script is thread safe.
74
+ # if you use thread-unsafe code in your script you are responsible
75
+ # for synchronization.
76
+ #
77
+ # TODO:
78
+ # add timing code here
79
+ #
80
+ def process(request)
81
+ # gmosx, FIXME: temporarily here, move somewhere ELSE!
82
+
83
+ request.locale = $lc_map[request.user.locale] || $lc_en
84
+ request.shader = calc_shader(request)
85
+ request.tag = calc_tag(request)
86
+ request.top_script = script = get_compiled_script(request.path, request.tag, request.shader)
87
+
88
+ # Action requests should be uncacheable, to be safe.
89
+ # No need for a check here! all actions should use consume which
90
+ # sets the uncacheable flag.
91
+ #
92
+ # request.uncacheable = true if request.delete("*act*")
93
+ # request.uncacheable = true if request.query_string =~ /\$.*\$/
94
+
95
+ script.__init_render(request)
96
+
97
+ # dont cache admin pages, FIXME: use less checks here!!
98
+
99
+ if script.__cache?(request) and (not request.uncacheable) and
100
+ (not request.session["ADMIN_MODE"]) and (not $reload_scripts)
101
+ if etag = script.__etag(request)
102
+ request.headers["cache-control"] = "pubic, must-revalidate"
103
+ request.headers["etag"] = etag
104
+ end
105
+ else
106
+ # gmosx, FIXME: add the correct cache control header here!!
107
+ etag = nil
108
+ end
109
+
110
+ if etag && etag == request.headers["IF-NONE-MATCH"]
111
+ request.set_not_modified!
112
+ else
113
+ fragment = eval_script(script, request)
114
+ end
115
+
116
+ # walk the filter pipeline
117
+ super
118
+
119
+ return fragment, script
120
+ end
121
+
122
+ # Process sub level scripts.
123
+ #
124
+ def sub_process(request)
125
+ script = get_compiled_script(request.path, request.tag, request.shader)
126
+ script.__init_render(request)
127
+ fragment = eval_script(script, request)
128
+
129
+ return fragment, script
130
+ end
131
+
132
+ # evaluate the request script in the context of the current request.
133
+ # returns the output of the script, ie the fragment body.
134
+ #
135
+ # === Design:
136
+ #
137
+ # I think that the script caching logic should be here, because it nicelly
138
+ # encapsulates transform and compile.
139
+ #
140
+ def eval_script(script, request)
141
+
142
+ etag = script.__etag(request)
143
+
144
+ unless fragment = script.__cache_get(etag)
145
+
146
+ # no suitable fragment exists in the cache, so render the script.
147
+ $log.debug "Rendering, #{request.path}" if $DBG
148
+
149
+ fragment = Fragment.new
150
+
151
+ # try to render the script, report errors.
152
+ begin
153
+ fragment.body = script.__render(request)
154
+
155
+ if script.__cache?(request)
156
+ script.__cache_put(etag, fragment) unless request.uncacheable
157
+ end
158
+ rescue N::ScriptExitException => see
159
+ # the script raised a ScripteExitException to force a premature
160
+ # end. Surpress this error!
161
+ rescue => ex
162
+ log_error(request, ex)
163
+ end
164
+
165
+ end
166
+
167
+ return fragment
168
+ end
169
+
170
+ # === Output:
171
+ # defcode: the code that customizes the base script class.
172
+ # pagecode: the code that renders the fragment.
173
+ #
174
+ # === TODO:
175
+ # - we have to keep the original script code to apply multiple shaders
176
+ #
177
+ # === REMARKS:
178
+ # - this method is evaluated in compile time, so we cannot pass
179
+ # or use the request/request pair. gmosx: NO, we do pass request,
180
+ # some data can and SHOULD be used to prepare the transform.
181
+ #
182
+ def transform_script(path, hash, shader = nil)
183
+
184
+ path = overload_path(path)
185
+
186
+ initcode = ""
187
+ defcode = ""
188
+ sub_scripts = []
189
+ filename = "#$root_dir/#{path}"
190
+ cached_filename = ".cache/#{shader}#{path}.rb"
191
+
192
+ # load the page text from the script.
193
+ # check if a cached transformed script exists.
194
+ # also staticaly includes all <?include xxx ?> files.
195
+ # this also validates the xhtml (cool side-effect).
196
+ begin
197
+ document = ::File.read(filename)
198
+
199
+ # calculate mtime
200
+ mtime = ::File.stat(filename).mtime
201
+ document.scan(/<\?include xl:href="(.*?)"(.*)\?>/) { |match|
202
+ match = overload_path(match[0])
203
+ imtime = ::File.stat("#$root_dir/#{match}").mtime
204
+ mtime = imtime if imtime > mtime
205
+ }
206
+ # also check the shader mtime
207
+ # FIXME: even better should check mtime now!
208
+ mtime = shader.mtime if shader.mtime > mtime
209
+
210
+ # calculate subscripts
211
+ document.scan(/<x:include xl:href="(.*?)"(.*)>/) { |match|
212
+ match = overload_path(match[0])
213
+ sub_path = "#{match.split("?")[0]}"
214
+ sub_scripts << get_compiled_script(sub_path, hash, shader)
215
+ }
216
+
217
+ # check if a cached transformed script exists
218
+ # FIXME: encode shader and shader mtime.
219
+ if false # ::File.exists?(cached_filename) and ::File.stat(cached_filename).mtime > mtime
220
+ return ::File.read(cached_filename), sub_scripts
221
+ end
222
+
223
+ $log.debug "Transforming '#{path}'" if $DBG
224
+
225
+ # static includes
226
+ # the target file is included at compile time.
227
+ #
228
+ # gmosx: must be xformed before the <?r pi.
229
+ # ex:
230
+ # <?include xl:href="root/myfile.sx" ?>
231
+ #
232
+ document.gsub!(/<\?include xl:href="(.*?)"(.*)\?>/) { |match|
233
+ # gmosx: xmm match matches the whole string.
234
+ match = overload_path($1)
235
+ load_statically_included("#$root_dir/#{match}")
236
+ }
237
+
238
+ # dynamic includes
239
+ # the target file is included at run time
240
+ #
241
+ document.gsub!(/<x:include xl:href="(.*?)"(.*)(.?)\/>/) { |match|
242
+ "<?r __out << __include('#$1', request) ?>"
243
+ }
244
+
245
+ # expand method macro
246
+ #
247
+ document.gsub!(/\@\?(.*?)(['|"|\s])/, '#{_a(request, %^\1^)}\2')
248
+
249
+ # localisation
250
+ # gmosx, OPTIMIZE in the future i could pre translate the strings!
251
+ document.gsub!(/\|:(.*?)\|/, '#{lc[:\1]}')
252
+ document.gsub!(/\!:(.*?)\|(.*?)\!/, '#{lc[:\1].call(\2)}')
253
+
254
+ # extract the definition code. The <?def .. ?> will
255
+ # be ignored afterwards.
256
+ #
257
+ document.scan(/<\?def(.*?)\?>/m) { |match|
258
+ defcode << $1 << "\n"
259
+ }
260
+
261
+ # extract the initialization phase code. The <?i .. ?> will
262
+ # be ignored afterwards.
263
+ #
264
+ document.scan(/<\?i(.*?)\?>/m) { |match|
265
+ initcode << $1 << "\n"
266
+ }
267
+
268
+ rescue Exception, StandardError => e
269
+ $log.error pp_exception(e)
270
+ raise RuntimeError.new
271
+ end
272
+
273
+ # pre-transform the script.
274
+ pagecode = shader.transform(document)
275
+
276
+ # preprocess the script to convert to valid ruby code.
277
+
278
+ # strip the xml header! (interracts with the following gsub!)
279
+ # FIXME: perhaps the xslt could strip this?
280
+ pagecode.gsub!(/<\?xml.*\?>/, "")
281
+
282
+ # xform the processing instructions
283
+ pagecode.gsub!(/\?>/, "\n__out << %{")
284
+ pagecode.gsub!(/<\?ruby /, "}\n")
285
+ pagecode.gsub!(/<\?r /, "}\n")
286
+
287
+ # tml, TODO: resolve static injects! scan injects and update the metadata
288
+ # in the page-graph.
289
+
290
+ pagecode = %@__out << %{#{pagecode}}@
291
+
292
+ # gmosx: unescape quotes etc, that are escaped by the xslt processor.
293
+ # can we avoid this ???
294
+ # yes: the xslt processors escapes code in #{ } brackets, we should
295
+ # use <?= ?> brackets.
296
+ #
297
+ # The new version of render supports output buffering ala php.
298
+ # __out_buffers keeps a stack of buffers.
299
+
300
+ pagecode = CGI.unescapeHTML(pagecode)
301
+ key = calc_script_key(path, hash)
302
+
303
+ script = %{
304
+ class PageScript#{key} < PageScript
305
+ def initialize(path)
306
+ super
307
+ @key = "#{key}"
308
+ end
309
+ #{defcode}
310
+ def __init_render(request)
311
+ #{initcode}
312
+ end
313
+ def __render(request)
314
+ lc = request.locale
315
+
316
+ __out_buffers = nil
317
+
318
+ __out = ""
319
+ if request.is_top?
320
+ __out = %|<?xml version="1.0"?>
321
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">|
322
+ end
323
+
324
+ #{pagecode}
325
+ return __out
326
+ end
327
+ end
328
+ compiled_script = PageScript#{key}.new("#{path}")
329
+ }
330
+
331
+ # remove excessive white space
332
+ # gmosx: FIXME: i can do better here: remove leading space per
333
+ # line for example.
334
+ script.squeeze!(" \t")
335
+
336
+ # cache the transformed script.
337
+ #
338
+ dir = N::StringUtils.directory_from_path(cached_filename)
339
+ FileUtils.mkdir_p(dir)
340
+ File.open(cached_filename, "w") { |f|
341
+ f << script
342
+ }
343
+
344
+ return script, sub_scripts
345
+ end
346
+
347
+ # try to get the script from the cache. invalidates the compiled version and return nil
348
+ # if the script is modified since compile time. Also takes active shader and localization
349
+ # into account. If script is not compiled, transform and compile it.
350
+ #
351
+ # Output:
352
+ # the compiled script. Throws exception if the script does not exist.
353
+ #
354
+ def get_compiled_script(path, hash, shader)
355
+ compiled_script = nil
356
+ key = calc_script_key(path, hash)
357
+
358
+ # gmosx: $reload_scripts is typically set to true when debugging
359
+ # statically included files (.ss)
360
+ unless $reload_scripts
361
+
362
+ compiled_script = @@compiled_script_cache[key]
363
+
364
+ # gmosx: monitor scripts should be explicit!
365
+ if $srv_monitor_scripts and compiled_script and (File.mtime(compiled_script.path) > compiled_script.__create_time)
366
+ $log.debug "Script '#{path}' externaly modified, recompiling" if $DBG
367
+ compiled_script = nil
368
+ end
369
+
370
+ end
371
+
372
+ unless compiled_script
373
+ # the script is not cached, so load, transform and compile it!
374
+ $log.debug "Compiling script '#{key}'" if $DBG
375
+
376
+ script, sub_scripts = transform_script(path, hash, shader)
377
+ compiled_script = compile_script(script)
378
+ compiled_script.sub_scripts = sub_scripts unless sub_scripts.empty?
379
+
380
+ @@compiled_script_cache[key] = compiled_script
381
+ end
382
+
383
+ return compiled_script
384
+ end
385
+
386
+ #-----------------------------------------------------------------------------
387
+ # Hooks
388
+
389
+ # Calculate shader for this request.
390
+ #
391
+ # TODO: allow per include shader!
392
+ #
393
+ def calc_shader(request)
394
+ return $default_shader
395
+ # return $shaders[request.user.shader]
396
+ end
397
+
398
+ # Standard encoding/modification/customization of the fragment_hash.
399
+ # Override this in your application to provide customized encoding.
400
+ # For example encode the shader id.
401
+ #
402
+ def calc_tag(request)
403
+ return "#{request.locale[:locale]}#{request.shader.name}"
404
+ end
405
+
406
+ def calc_script_key(path, hash)
407
+ return "#{path}#{hash}".gsub(/[^\w]/, "")
408
+ end
409
+
410
+ #-----------------------------------------------------------------------------
411
+ # Utils
412
+
413
+ # loads a script and prepares it for statically inclusion by removing
414
+ # the optional xml prologue/epilogue.
415
+ #
416
+ def load_statically_included(filename)
417
+ $log.debug "Statically including '#{filename}'" if $DBG
418
+
419
+ code = File.read(filename)
420
+ code.gsub!(/<\?xml.*\?>/, "")
421
+ code.gsub!(/<\/?root(.*?)>/m, " ");
422
+
423
+ return code
424
+ end
425
+
426
+ end # PageHandler
427
+
428
+ # = Pagescript
429
+ #
430
+ # encapsulates the script defining a specific xml page.
431
+ # effectively acts as a Generator.
432
+ #
433
+ # === Requirements:
434
+ #
435
+ # - the generated class should be a singleton, no need to stress
436
+ # the garbage collector.
437
+ # - the render method should be thread safe.
438
+ #
439
+ # == Design:
440
+ #
441
+ # - only pass request and request for simplicity
442
+ # __session can be deducted from request, and __user/__errors from
443
+ # __session.
444
+ #
445
+ # === Injection:
446
+ #
447
+ # Injection is the inclusion of subscripts in a super (parent) script.
448
+ # There are two types of inclusion, dynamic and static. In contrary to what
449
+ # you may believe, dynamix inclusion IS needed in cases where you decide
450
+ # what tou include at runtime. A synthesized "my" page is a good example.
451
+ # However, n1 overused dynamic inclusion. In most cases static inclusion
452
+ # works just fine. Another interesting optimization is when you dynamically
453
+ # include but not recursivelly. The full, recursive and dynamic include is
454
+ # really seldomly needed. However in our implementation there is no difference
455
+ # between inject_once / inject_recursive.
456
+ #
457
+ # === Class generation example:
458
+ #
459
+ # WARNING: the example may be deprecated!
460
+ #
461
+ # source script:
462
+ #
463
+ # <?xml version='1.0'?>
464
+ # <?ruby
465
+ # a = 5
466
+ # b = request["bval"]
467
+ # ?>
468
+ # <html>
469
+ # <body>
470
+ # this is it a = #{a}, b = #{b} and c = #{request["cval"]}<br/>
471
+ # cool huh?
472
+ # </body>
473
+ # </html>
474
+ #
475
+ # generated class:
476
+ #
477
+ # class PageScript__index
478
+ # def render(request)
479
+ # out = ""
480
+ # a = 5
481
+ # b = request["bval"]
482
+ # __out << %{
483
+ # <html>
484
+ # <body>
485
+ # this is it a = #{a}, b = #{b} and c = #{request["cval"]}<br/>
486
+ # cool huh?
487
+ # </body>
488
+ # </html>
489
+ # }
490
+ # return __out
491
+ # end
492
+ # end
493
+ #
494
+ # === Future:
495
+ #
496
+ # - use catbuffer for optimized appends.
497
+ #
498
+ class PageScript < Script
499
+ # <x:include xl:href='...'/> implementation
500
+ # Dynamically include ("inject") a subpage (fragment) in this page.
501
+ #
502
+ # gmosx, SOS: This is a new version for dynamic inclusion that doesnt
503
+ # generate subrequest objects. For use with new (v3) code.
504
+ #
505
+ # === Design:
506
+ #
507
+ # To keep this method simple (and as a small optimization) we used
508
+ # to not allow a query string when including. Pass the parameters
509
+ # as request arguments or parameters. Here is an example:
510
+ #
511
+ # <?r request_set_arg("max_message", 5); request["admin"] = true ?>
512
+ # <x:include xl:href='*parts/messages/view-messages.xi'/>
513
+ #
514
+ # However to support legacy code, to follow the REST design guidelines,
515
+ # and because it turned out to be easy thanks to the refactoring, the
516
+ # query string is supported. Passing params through the request object
517
+ # is suggested though (and essential to pass ruby objects). The query
518
+ # string of the included string is converted to arguments to avoid
519
+ # poluting the parent request query string.
520
+ #
521
+ # Still, the preferred way is to use the args hash to pass special
522
+ # arguments to included scripts.
523
+ # <?r request.set_arg("maxitems", 5) ?>
524
+ # <x:include xl:href="p/list.si" />
525
+ #
526
+ # If you want to include the parents query string for caching purposes
527
+ # call with parent = true!
528
+ #
529
+ # TODO: add unit test for this.
530
+ #
531
+ def __include(uri, request, parent = false)
532
+ begin
533
+ $log.devel "Including #{uri}"
534
+
535
+ # get script real path and type
536
+ script_path, type, args, qs = N::UriUtils.decode(uri)
537
+
538
+ # update the dependencies set, we only need the key as flag,
539
+ # so just insert true. Calculating the dependencies this
540
+ # way is simple and not too inefficient.
541
+ #
542
+ # @dependencies[script_path] = true
543
+
544
+ request.level += 1
545
+ request.path = script_path
546
+
547
+ # Build the cache key for this request.
548
+ # attach the query string of the parent. Needed for
549
+ # example in fragments with pagers. Do NOT do this
550
+ # by default, or excessive numbers of fragments will
551
+ # be generated.
552
+ if parent
553
+ request.fragment_hash = "#{qs}#{request.query_string}"
554
+ else
555
+ request.fragment_hash = qs
556
+ end
557
+
558
+ # add arguments passes through the query string.
559
+ request.parameters.update(args)
560
+
561
+ # TODO: select by regexp
562
+ # FIXME: hacky implementation, NO Need for this test, always
563
+ # PageHandler.
564
+ if handler = $srv_extension_map[type]
565
+ handler = handler[1]
566
+ else
567
+ $log.error "No handler for extension '#{type}'"
568
+ unless N::StringUtils.valid?(type)
569
+ $log.error "Perhaps you have a syntax error in your include statement or you didnt pass the uri"
570
+ end
571
+ end
572
+
573
+ fragment, script = handler.sub_process(request)
574
+
575
+ # restore parent request
576
+ request.level -= 1
577
+
578
+ # gmosx: one idea was to clear the args here to avoid a class of bugs.
579
+ # But i think it is better to trust the developer to do this, thus
580
+ # giving him greater flexibility.
581
+ # gmosx: NO !!! practice shows that it is NOT GOOD to trust the
582
+ # developer :)
583
+ # Hmm clearing the args also poses problems though :(
584
+ #
585
+ # request.args.clear()
586
+
587
+ return fragment.body
588
+
589
+ rescue ScriptHandlerError => e
590
+ # allready handled!
591
+ rescue Exception, StandardError => e
592
+ # gmosx: this block is used to catch possible errors in the inject
593
+ # implementation if we do not catch these errors here, they
594
+ # propagate to the caller that prints a NON usefull message, that
595
+ # can waste a developers time.
596
+ #
597
+ $log.error "error in INCLUDE IMPLEMENTATION while including: #{uri}"
598
+ # gmosx: too noisy, only uncomment if you get the above error!!
599
+ $log.error pp_exception(e)
600
+
601
+ # Following our design goal of chaos reduction (ie small changes
602
+ # should result in small results) we drink the exception and
603
+ # output an error flag. So the rest of the page is rendered and
604
+ # the server error handler is NOT triggered.
605
+ end
606
+
607
+ return "(error)"
608
+ end
609
+
610
+ end # PageScript
611
+
612
+ end; end # module