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