Class: N::App::PageHandler

Inherits:
ScriptHandler show all
Defined in:
lib/n/app/handlers/page-handler.rb

Overview

PageHandler

web server handler that render xml pages (.sx scripts). This is the main handler of the Nitro Application Server.

The handler evaluates the given script. The result of this evaluation is called a Fragment. The result of a top level script, ie a top level fragments is called a page.

Advantages over the original .rx format:

  • xml based

  • strips <!– comments

  • allows multiline comments

  • allows pre-transformation with xlst (free transformation)

  • allows validation of xhtml

  • allows syntax highlighting

TODO:

  • move shader selection in a script method, that can be overriden (user specific shaders).

  • compress the output some more, every byte

counts (bandwidth == money)

  • use something like FormValidator to validate parameters

for sub-fragments.

FIXME:

  • correct encoding of fragment hash

(query string, shader, user etc)

Design:

break the rendering process in sub methods to make testable and verifiable (and easier to read).

Statically included scripts (.ss) SHOULD be valid xml files (even though they are not required) to be automatically verifiable and compatible with editors. The xml prologue (<?xml / <root>) is removed.

Be carefull about statically included files scope issues. This is the reason why we do not use statically included files everywhere.

WARNING:

no need to lock cache io, we use a safe cache!

Instance Attribute Summary

Attributes inherited from ServerFilter

#next_filter

Instance Method Summary collapse

Methods inherited from ScriptHandler

#compile_script, #compiled_script_cache, #log_error, #overload_path

Methods inherited from ServerFilter

#<<, #initialize, #process_next

Constructor Details

This class inherits a constructor from N::ServerFilter

Instance Method Details

#calc_script_key(path, hash) ⇒ Object



406
407
408
# File 'lib/n/app/handlers/page-handler.rb', line 406

def calc_script_key(path, hash)
	return "#{path}#{hash}".gsub(/[^\w]/, "")
end

#calc_shader(request) ⇒ Object

Calculate shader for this request.

TODO: allow per include shader!



393
394
395
396
# File 'lib/n/app/handlers/page-handler.rb', line 393

def calc_shader(request)
	return $default_shader		
	# return $shaders[request.user.shader]
end

#calc_tag(request) ⇒ Object

Standard encoding/modification/customization of the fragment_hash. Override this in your application to provide customized encoding. For example encode the shader id.



402
403
404
# File 'lib/n/app/handlers/page-handler.rb', line 402

def calc_tag(request)
	return "#{request.locale[:locale]}#{request.shader.name}"
end

#eval_script(script, request) ⇒ Object

evaluate the request script in the context of the current request. returns the output of the script, ie the fragment body.

Design:

I think that the script caching logic should be here, because it nicelly encapsulates transform and compile.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/n/app/handlers/page-handler.rb', line 140

def eval_script(script, request)

	etag = script.__etag(request)

	unless fragment = script.__cache_get(etag)

		# no suitable fragment exists in the cache, so render the script.
		$log.debug "Rendering, #{request.path}" if $DBG

		fragment = Fragment.new
		
		# try to render the script, report errors.
		begin
			fragment.body = script.__render(request)

			if script.__cache?(request)
				script.__cache_put(etag, fragment) unless request.uncacheable
			end
		rescue N::ScriptExitException => see
			# the script raised a ScripteExitException to force a premature
			# end. Surpress this error!
		rescue => ex
			log_error(request, ex)
		end

	end
	
	return fragment
end

#get_compiled_script(path, hash, shader) ⇒ Object

try to get the script from the cache. invalidates the compiled version and return nil if the script is modified since compile time. Also takes active shader and localization into account. If script is not compiled, transform and compile it.

Output: the compiled script. Throws exception if the script does not exist.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/n/app/handlers/page-handler.rb', line 354

def get_compiled_script(path, hash, shader)
	compiled_script = nil
	key = calc_script_key(path, hash)

	# gmosx: $reload_scripts is typically set to true when debugging
	# statically included files (.ss)
	unless $reload_scripts
	
		compiled_script = @@compiled_script_cache[key]

		# gmosx: monitor scripts should be explicit!
		if $srv_monitor_scripts and compiled_script and (File.mtime(compiled_script.path) > compiled_script.__create_time)
			$log.debug "Script '#{path}' externaly modified, recompiling" if $DBG
			compiled_script = nil
		end
		
	end

	unless compiled_script
		# the script is not cached, so load, transform and compile it!
		$log.debug "Compiling script '#{key}'" if $DBG

		script, sub_scripts = transform_script(path, hash, shader)
		compiled_script = compile_script(script)
		compiled_script.sub_scripts = sub_scripts unless sub_scripts.empty?
		
		@@compiled_script_cache[key] = compiled_script
	end
	
	return compiled_script
end

#load_statically_included(filename) ⇒ Object

loads a script and prepares it for statically inclusion by removing the optional xml prologue/epilogue.



416
417
418
419
420
421
422
423
424
# File 'lib/n/app/handlers/page-handler.rb', line 416

def load_statically_included(filename)
	$log.debug "Statically including '#{filename}'" if $DBG
	
	code = File.read(filename)
	code.gsub!(/<\?xml.*\?>/, "")
	code.gsub!(/<\/?root(.*?)>/m, " ");
	
	return code
end

#process(request) ⇒ Object

process is called ONLY for top level scripts.

no need for synchronization, the page-script is thread safe. if you use thread-unsafe code in your script you are responsible for synchronization.

TODO: add timing code here



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/n/app/handlers/page-handler.rb', line 80

def process(request)	
	# gmosx, FIXME: temporarily here, move somewhere ELSE!

	request.locale = $lc_map[request.user.locale] || $lc_en
	request.shader = calc_shader(request)
	request.tag = calc_tag(request)
	request.top_script = script = get_compiled_script(request.path, request.tag, request.shader)
	
	# Action requests should be uncacheable, to be safe.
	# No need for a check here! all actions should use consume which
	# sets the uncacheable flag.
	#
	# request.uncacheable = true if request.delete("*act*")
	# request.uncacheable = true if request.query_string =~ /\$.*\$/
	
	script.__init_render(request)
	
	# dont cache admin pages, FIXME: use less checks here!! 
	
	if script.__cache?(request) and (not request.uncacheable) and 
			(not request.session["ADMIN_MODE"]) and (not $reload_scripts)
		if etag = script.__etag(request)
			request.headers["cache-control"] = "pubic, must-revalidate"
			request.headers["etag"] = etag
		end
	else
		# gmosx, FIXME: add the correct cache control header here!!
		etag = nil
	end

	if etag && etag == request.headers["IF-NONE-MATCH"]
		request.set_not_modified!
	else
		fragment = eval_script(script, request)
	end

	# walk the filter pipeline
	super
	
	return fragment, script
end

#sub_process(request) ⇒ Object

Process sub level scripts.



124
125
126
127
128
129
130
# File 'lib/n/app/handlers/page-handler.rb', line 124

def sub_process(request)
	script = get_compiled_script(request.path, request.tag, request.shader)
	script.__init_render(request)
	fragment = eval_script(script, request)
	
	return fragment, script
end

#transform_script(path, hash, shader = nil) ⇒ Object

Output:

defcode: the code that customizes the base script class. pagecode: the code that renders the fragment.

TODO:

  • we have to keep the original script code to apply multiple shaders

REMARKS:

  • this method is evaluated in compile time, so we cannot pass or use the request/request pair. gmosx: NO, we do pass request, some data can and SHOULD be used to prepare the transform.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/n/app/handlers/page-handler.rb', line 182

def transform_script(path, hash, shader = nil)

	path = overload_path(path)
	
	initcode = ""
	defcode = ""
	sub_scripts = []
	filename = "#$root_dir/#{path}"
	cached_filename = ".cache/#{shader}#{path}.rb"

	# load the page text from the script.
	# check if a cached transformed script exists.
	# also staticaly includes all <?include xxx ?> files.
	# this also validates the xhtml (cool side-effect).
	begin
		document = ::File.read(filename)

		# calculate mtime
		mtime = ::File.stat(filename).mtime		
		document.scan(/<\?include xl:href="(.*?)"(.*)\?>/) { |match|
			match = overload_path(match[0])
			imtime = ::File.stat("#$root_dir/#{match}").mtime
			mtime = imtime if imtime > mtime
		}
		# also check the shader mtime
		# FIXME: even better should check mtime now!
		mtime = shader.mtime if shader.mtime > mtime
	
		# calculate subscripts
		document.scan(/<x:include xl:href="(.*?)"(.*)>/) { |match|
			match = overload_path(match[0])				
			sub_path = "#{match.split("?")[0]}"
			sub_scripts << get_compiled_script(sub_path, hash, shader)
		}

		# check if a cached transformed script exists
		# FIXME: encode shader and shader mtime.
		if false # ::File.exists?(cached_filename) and ::File.stat(cached_filename).mtime > mtime
			return ::File.read(cached_filename), sub_scripts
		end

		$log.debug "Transforming '#{path}'" if $DBG
		
		# static includes
		# the target file is included at compile time.
		#
		# gmosx: must be xformed before the <?r pi.
		# ex:		
		# <?include xl:href="root/myfile.sx" ?>
		#
		document.gsub!(/<\?include xl:href="(.*?)"(.*)\?>/) { |match|
			# gmosx: xmm match matches the whole string.
			match = overload_path($1)				
			load_statically_included("#$root_dir/#{match}")
		}

		# dynamic includes
		# the target file is included at run time
		#
		document.gsub!(/<x:include xl:href="(.*?)"(.*)(.?)\/>/) { |match|
			"<?r __out << __include('#$1', request) ?>"
		}
		
		# expand method macro 
		#
		document.gsub!(/\@\?(.*?)(['|"|\s])/, '#{_a(request, %^\1^)}\2')

		# localisation 
		# gmosx, OPTIMIZE in the future i could pre translate the strings!
		document.gsub!(/\|:(.*?)\|/, '#{lc[:\1]}')
		document.gsub!(/\!:(.*?)\|(.*?)\!/, '#{lc[:\1].call(\2)}')

		# extract the definition code. The <?def .. ?> will
		# be ignored afterwards.
		#
		document.scan(/<\?def(.*?)\?>/m) { |match|
			defcode << $1 << "\n"
		}
		
		# extract the initialization phase code. The <?i .. ?> will
		# be ignored afterwards.
		#
		document.scan(/<\?i(.*?)\?>/m) { |match|
			initcode << $1 << "\n"
		}
		
	rescue Exception, StandardError => e
		$log.error pp_exception(e)
		raise RuntimeError.new
	end

	# pre-transform the script.
	pagecode = shader.transform(document)

	# preprocess the script to convert to valid ruby code.

	# strip the xml header! (interracts with the following gsub!)
	# FIXME: perhaps the xslt could strip this?
	pagecode.gsub!(/<\?xml.*\?>/, "")

	# xform the processing instructions
	pagecode.gsub!(/\?>/, "\n__out << %{")
	pagecode.gsub!(/<\?ruby /, "}\n")
	pagecode.gsub!(/<\?r /, "}\n")
	
	# tml, TODO: resolve static injects! scan injects and update the metadata
	# in the page-graph.

	pagecode = %@__out << %{#{pagecode}}@

	# gmosx: unescape quotes etc, that are escaped by the xslt processor.
	# can we avoid this ???
	# yes: the xslt processors escapes code in #{ } brackets, we should
	# use <?= ?> brackets.
	#
	# The new version of render supports output buffering ala php.
	# __out_buffers keeps a stack of buffers.
	
	pagecode = CGI.unescapeHTML(pagecode)
	key = calc_script_key(path, hash)
	
	script = %{
		class PageScript#{key} < PageScript
			def initialize(path)
				super
				@key = "#{key}"
			end
			#{defcode}
			def __init_render(request)
			#{initcode}
			end
			def __render(request)
				lc = request.locale
				
				__out_buffers = nil
				
				__out = ""					
				if request.is_top?
					__out = %|<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">|
				end
				
				#{pagecode}
				return __out
			end
		end			
		compiled_script = PageScript#{key}.new("#{path}")
	}
	
	# remove excessive white space
	# gmosx: FIXME: i can do better here: remove leading space per
	# line for example.
	script.squeeze!(" \t")

	# cache the transformed script.
	#
	dir = N::StringUtils.directory_from_path(cached_filename)
	FileUtils.mkdir_p(dir)		
	File.open(cached_filename, "w") { |f|
		f << script	
	}	

	return script, sub_scripts		
end