require 'sync'
require 'glue/attribute'
require 'glue/misc'
require 'glue/object'
require 'nitro/shaders'
require 'nitro/buffering'
require 'nitro/errors'
module Nitro
# Raise this exception to stop the current action.
# Typically called to skip the template.
class ActionExit < Exception; end
# Raise this exception to stop rendering altogether.
# Typically called by redirects.
class RenderExit < Exception; end
# Rendering utility methods
module Rendering
@@sync = Sync.new
# The default template root
mattr_accessor :default_template_root, 'public'
# The default template name (no extension).
mattr_accessor :default_template, 'index'
# The shader used for transforming templates.
# The default shader is very simple, here is a
# typical example of a production shader pipeline:
#
#
# Rendering.shader =
# XSLTShader.new("xsl/style.xsl",
# RubyShader.new(
# CompressShader.new
# )
# )
#
mattr_accessor :shader; @@shader = RubyShader.new
# If set to :full, reloads all controllers. Useful in
# development.
mattr_accessor :reload, false
# Given the action try find the matching template.
# Can search for xhtml or xml templates.
# Returns nil if no template file is found.
def self.template_for_action(template_root, action, ext = :xhtml)
# attempt to find a template of the form
# template_root/action.xhtml
path = "#{template_root}/#{action.gsub(/__/, '/')}.#{ext}".squeeze('/')
unless File.exist?(path)
# attempt to find a template of the form
# template_root/action/index.xhtml
path = "#{template_root}/#{action.gsub(/__/, '/')}/#{Rendering.default_template}.#{ext}".squeeze('/')
unless File.exist?(path)
# No template found!
path = nil
end
end
return path
end
# Transform a template to ruby rendering code.
def self.transform_template(path, shader)
Logger.debug "Transforming '#{path}'" if $DBG
text = File.read(path)
hash, text = shader.process(path, text)
return text
end
# Compile a controller action.
def self.compile_action(klass, action)
@@sync.synchronize do
Aspects.include_advice_modules(klass)
action = action.to_s.gsub(/_action$/, '')
# This is not a controller action.
return false unless action
Logger.debug "Compiling action '#{klass.template_root}/#{action}'" if $DBG
valid = false
code = %{
def #{action}_action
@parent_action_name = @action_name
@action_name = '#{action}'
}
# Inject the pre advices.
code << Aspects.gen_advice_code(action, klass.advices, :pre)
# Call the action
if klass.action_methods.include?(action)
valid = true
# Annotated parameters.
if meta = klass.action_metadata[action.intern]
params = meta.params.keys
params = params.collect { |p| "@#{p} = @context['#{p}']" }
code << "#{params.join(';')}"
end
# Try to resolve action parameters.
param_count = klass.instance_method(action.intern).arity
if param_count > 0
code << %{
qs = context.query_string.split(/[&;]/)
params = []
#{param_count}.times do |i|
params << qs.shift.split(/=/).last
end
unless :stop == #{action}(*params)
}
else
code << %{
unless :stop == #{action}
}
end
end
# Try to call the template method if it exists. It is a nice
# practice to put output related code in this method instead
# of the main action so that this method can be overloaded
# separately.
#
# If no template method exists, try to convert an external
# template file into a template method. It is an even better
# practice to place the output related code in an external
# template file.
# Take :view metadata into account.
view = nil
if md = klass.action_metadata[action.intern]
view = md[:view]
end
view ||= action
cklass = klass
while cklass.respond_to?(:template_root)
if template = template_for_action(cklass.template_root, view.to_s)
valid = true
code << %{
#{action}_template;
}
break
end
# don't search in parent template roots if an
# action is defined.
break if valid
cklass = cklass.superclass
end
# raise "Invalid action '#{action}' for '#{klass}'!" unless valid
return false unless valid
# Inject the post advices.
code << Aspects.gen_advice_code(action, klass.advices, :post)
if klass.action_methods.include?(action)
code << %{
end
}
end
code << %{
@action_name = @parent_action_name
redirect_referer if @out.empty?
end
}
# First compile the action method.
# begin
klass.class_eval(code)
# rescue SyntaxError => e
# raise ActionCompileError.new(code, action, e)
# end
# Try to compile the template (if exists).
if template
code = %{
def #{action}_template
#{transform_template(template, Rendering.shader)}
end
}
begin
klass.class_eval(code, template)
rescue SyntaxError => e
raise TemplateCompileError.new(code, template, e)
end
end
end
return true
end
end
# The rendering mixin.
#--
# TODO: handle template_root here instead of the
# controller.
#++
module Render
# The output buffer. The output of a script/action is
# accumulated in this buffer.
attr_accessor :out
# A nice alias
alias_method :body, :out
# The context.
attr_accessor :context
alias_method :ctx, :context
alias_method :ctx=, :context=
# Aliases for context.
attr_accessor :request, :response
# An array holding the rendering errors for this
# request.
attr_accessor :rendering_errors
# The name of the currently executing action.
attr_accessor :action_name
# Initialize the render.
#
# [+context+]
# A parent render/controller acts as the context.
def initialize(context, base = nil)
@request = @response = @context = context
@out = context.out
end
# Renders the action denoted by path. The path
# is resolved by the dispatcher to get the correct
# controller.
def render(path)
Logger.debug "Rendering '#{path}'." if $DBG
klass, action, base = @context.dispatcher.dispatch(path, @context)
# FIXME:
@context.content_type = klass.instance_variable_get('@content_type') || 'text/html'
raise 'No controller for action' unless klass
if self.class == klass
self.send(action)
else
klass.new(self, base).send(action)
end
rescue RenderExit => e
# Just stop rendering.
# For example called by redirects.
rescue Exception, StandardError => e
log_error(e, path)
# More fault tolerant, only flags the erroneous box with
# error not the full page.
@out << '(error)'
end
private
# Send a redirect response.
def redirect(url, status = 303)
url = url.to_s
url = "#{@context.host_url}/#{url.gsub(/^\//, '')}" unless url =~ /http/
@context.status = status
@context.out = "#{url}.\n"
@context.response_headers['location'] = url
raise RenderExit
end
# Redirect to the referer of this method.
def redirect_referer(postfix = nil, status = 303)
redirect("#{@context.referer}#{postfix}", status)
end
# Log a rendering error.
def log_error(error, path)
@rendering_errors ||= []
@rendering_errors << [error, path]
# gmosx: Hmm perhaps this should not be logged
# to avoid DOS attacks.
Logger.error "Error while handling '#{path}'."
Logger.error pp_exception(error)
end
# Convenience method to lookup the session.
def session
@context.session
end
# Convenience method to access the output buffer.
def o
@out
end
# Add some text to the output buffer.
def render_text(text)
@out << text
end
alias_method :print, :render_text
#--
# FIXME: do something better to stop the redirect.
#++
def render_nothing
@out = ' '
end
end
end
# * George Moschovitis