require 'singleton'
require 'sync'
require 'stringio'
require 'facet/string/blank'
require 'glue/attribute'
require 'glue/settings'
require 'glue/template'
require 'glue/builder/xml'
require 'nitro/helper/xhtml'
require 'nitro/helper/form'
require 'nitro/helper/table'
require 'nitro/helper/buffer'
module Nitro
# Raise or Throw this exception to stop the current action.
# Typically called to skip the template.
class ActionExit < Exception; end
# Raise or Throw this exception to stop rendering altogether.
# Typically called by redirects.
class RenderExit < Exception; end
# The output buffer. The output of a contoller action is
# accumulated in this buffer before sending this to the client
# as a HTTP Response.
#--
# TODO: Implement a FAST string (maybe in C)
#++
class OutputBuffer < String
end
# The rendering mixin. This module is typically included in
# published objects and/or controllers to provide rendering
# functionality.
#--
# TODO: handle template_root here instead of the
# controller.
#++
module Render
include BufferHelper
# If true, auto redirect to referer on empty buffer.
setting :redirect_on_empty, :default => false, :doc => 'If true, auto redirect to referer on empty buffer'
# The output buffer. The output of a script/action is
# accumulated in this buffer.
attr_accessor :out
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
# The base url for this render.
attr_accessor :base
# The name of the current controller.
#--
# gmosx: Needed for WEE. Will be deprecated.
#++
attr_accessor :controller_name
# Initialize the render.
#
# [+context+]
# A parent render/controller acts as the context.
def initialize(context, base = nil)
@rendering_context = 0
@request = @response = @context = context
@controller_name = @base = base
@out = context.out
end
# Renders the action denoted by path. The path
# is resolved by the dispatcher to get the correct
# controller.
#
# Both relative and absolute paths are supported. Relative
# paths are converted to absolute by prepending the @base
# path of the controller.
def render(path)
# Convert relative paths to absolute paths.
path = "#@base/#{path}" unless 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
@context.level += 1
if self.class == klass
self.send(action)
else
klass.new(@context, base).send(action)
end
@context.level -= 1
rescue NoActionError => e1
log_error(e1, path, false)
print '(error)'
rescue RenderExit, ActionExit => e2
# Just stop rendering. For example called by redirects.
rescue Exception, StandardError => e3
# More fault tolerant, only flags the erroneous box with
# error not the full page.
log_error(e3, path)
print '(error)'
end
private
# Helper method to exit the current action, typically used
# to skip the template.
def exit
raise ActionExit.new
end
# Flush the IO object if we are in streaming mode.
def flush
@out.flush if @out.is_a?(IO)
end
# :section: Redirection methods.
# Send a redirect response.
#
# If the url starts with '/' it is considered absolute, else
# the url is considered relative to the current controller and
# the controller base is prepended.
def redirect(url, status = 303)
url = url.to_s
unless url =~ /^http/
url = "#@base/#{url}" unless url =~ /^\//
url = "#{@context.host_url}/#{url.gsub(/^\//, '')}"
end
@context.status = status
@context.out = "#{url}.\n"
@context.response_headers['location'] = url
raise RenderExit
end
alias_method :redirect_to, :redirect
# Redirect to the referer.
def redirect_referer(postfix = nil, status = 303)
redirect("#{@context.referer}#{postfix}", status)
end
alias_method :redirect_to_referer, :redirect_referer
alias_method :redirect_referrer, :redirect_referer
alias_method :redirect_to_referrer, :redirect_referer
# Redirect to home.
def redirect_home(status = 303)
redirect('/', status)
end
alias_method :redirect_to_home, :redirect_home
# :section: Seaside style call/answer methods.
# Call redirects to the given url but push the original
# url in a callstack, so that the target can return by
# executing answer.
#
# === Example
#
# caller:
# color, type = call('utils/select_color')
#
# target:
# answer(color, type)
#--
# FIXME: dont use yet, you have to encode the branch to
# make this safe for use.
#++
def call(url, status = 303)
(session[:CALL_STACK] ||= {}) << request.uri
redirect(url, status)
end
# Returns from a call by poping the callstack.
#--
# FIXME: don't use yet.
#++
def answer(index = 0, status = 303)
if stack = session[:CALL_STACK] and not stack.empty?
redirect(stack.pop, status)
else
raise 'Cannot answer, call stack is empty'
end
end
# Log a rendering error.
def log_error(error, path, full = true)
(@rendering_errors ||= []) << [error, path]
if full
# gmosx: Hmm perhaps this should not be logged
# to avoid DOS attacks.
Logger.error "Error while handling '#{path}'."
Logger.error pp_exception(error)
else
Logger.error error.to_s
end
end
# Convenience method to lookup the session.
def session
@context.session
end
# Add some text to the output buffer.
def render_text(text)
@out << text
end
alias_method :print, :render_text
# Render a template into the output buffer.
def render_template(filename)
filename = "#{filename}.xhtml" unless filename =~ /\.xhtml$/
template = File.read("#{template_root}/#{filename}")
Glue::Template.process_template(template, '@out', binding)
end
# Access the programmatic renderer (builder).
def build(&block)
if block.arity == 1
yield Glue::XmlBuilder.new(@out)
else
Glue::XmlBuilder.new(@out).instance_eval(&block)
end
end
# Return a programmatic renderer that targets the
# output buffer.
def builder
Glue::XmlBuilder.new(@out)
end
# A Helper class to access rendering mixins. Useful to avoid
# poluting the Render with utility methods.
#--
# TODO: find a less confusing name.
#++
class Emitter
include Singleton
include XhtmlHelper
include FormHelper
include TableHelper
end
# A helper to access the utilities emitter:
#
# #{emit :form, entity}
# #{emit :options, :labels => [..], :values => [..], :selected => 1}
#
# Useful to avoid poluting the render with mixin methods.
def emit(meth, *options)
Emitter.instance.send(meth, *options)
end
end
end
# * George Moschovitis