nitro 0.1.2

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 (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