# * George Moschovitis # (c) 2004-2005 Navel, all rights reserved. # $Id$ require 'sync' require 'glue/attribute' require 'glue/misc' require 'glue/object' require 'nitro/shaders' require 'nitro/buffering' module N # Raise this exception to stop rendering. class RenderExit < Exception; end # Rendering utility methods module Rendering @@sync = Sync.new # 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}.#{ext}".squeeze('/') unless File.exist?(path) # attempt to find a template of the form # template_root/action/index.xhtml path = "#{template_root}/#{action}/#{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, template_root) @@sync.synchronize do dummy, api, action = action.to_s.split('__') # This is not a controller action. return false unless action Logger.debug "Compiling action '#{template_root}/#{action}'" if $DBG valid = false code = %{ def __#{api}__#{action} } # call 'before' filter chain. if klass.respond_to?(:before_filters) code << %{ #{klass.gen_filters_call_code(klass.before_filters)} } end # call the action if klass.action_methods.include?(action) valid = true if meta = klass.action_metadata[action.intern] params = meta.params.keys params = params.collect { |p| "@#{p} = @context['#{p}']" } code << "#{params.join(';')}" end code << %{ #{action} } end # call the programmatically generated template if exists. if klass.action_methods.include?("#{action}__#{api}") valid = true code << %{ return unless #{action}__#{api}(); } end # call the template if exists. if template = template_for_action(template_root, action) valid = true code << %{ return unless __#{api}__#{action}__template(); } end # raise "Invalid action '#{action}' for '#{klass}'!" unless valid return false unless valid # call 'after' filter chain. if klass.respond_to?(:after_filters) code << %{ #{klass.gen_filters_call_code(klass.after_filters)} } end code << %{ redirect_referer if @out.empty? end } if template code << %{ def __#{api}__#{action}__template #{transform_template(template, Rendering.shader)} end } end klass.class_eval(code) end return true end end # The rendering mixin. module Render # The outbut buffer. The output of a script/action is accumulated # in this buffer. attr_accessor :out # The context. attr_accessor :context alias_method :ctx, :context alias_method :ctx=, :context= # Alias for context. attr_accessor :request # An array holding the rendering errors for this # request. attr_accessor :rendering_errors # The template root for this render. cattr_accessor :template_root # Initialize the render. # # [+context+] # A parent render/controller acts as the context. def initialize(context) @request = @context = context @out = context.out @template_root = @context.dispatcher.template_root 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, template_root, ctype = @context.dispatcher.dispatch(path, @context) klass, action, content_type = @context.dispatcher.dispatch(path, @context) @context.content_type = content_type raise 'No controller for action' unless klass if self.class == klass self.send(action) else klass.new(self).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) @context.status = status @context.out = "#{url.to_s}.\n" @context.response_headers['location'] = url.to_s 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 end end