require 'thread'
require 'log4r'
include Log4r
require 'iowa/PageStore'
require 'iowa/SessionStats'
module Iowa
# Container for an inline generated resource.
class Resource
attr_accessor :body, :content_type
def initialize(body,content_type='application/data')
@body = body
@content_type = content_type
end
end
# Custom exception which is thrown if a request does not have a request ID.
class IgnoreRequest < Exception; end
# Custom exception which is thrown if a request asks for a page which has
# expired from the cache.
class PageExpired < Exception; end
# Represents a session between the application and the user.
class Session
# The default number of pages to cache. It'd be nice if this were
# easily configurable somehow, so apps could finetune their own
# caching behaviors.
@@cachedPages = 10
@@cacheTTL = nil
# Default delay between the presentation of the page expired message
# (see this below) and the redirect to the top of the application.
@@expiryDelay = 1
# Class variable which holds the class of a session.
@@sessionClass = Session
# Define an accessor to make the context object accessible to the
# application's code.
attr_accessor :context, :application, :currentPage, :requestCount, :pages,
:resourceCount
# Set the @cachedPages class variable.
def Session.cachedPages
@@cachedPages
end
def Session.cachedPages=(val)
@@cachedPages = val
end
def Session.cacheTTL
@@cacheTTL
end
def Session.cacheTTL=(val)
@@cacheTTL = val
end
# Constructor to return a new instance of @@sessionClass.
def Session.newSession(*args)
@@sessionClass.new(*args)
end
def Session.inherited(subclass)
@@sessionClass = subclass
end
# Initialize the state of the session.
def initialize
mylog = Logger['iowa_log']
@currentPage = nil
@requestCount = 'a'
@resourceCount = 'a'
@resources = {}
@resources_by_component = {}
@lock = Mutex.new
@creation_time = Time.now
@pages = PageStore.new(@@cachedPages,@@cacheTTL)
@pages.add_finalizer(@resources,@resources_by_component) {|key,obj,resources,resources_by_component|
if resources_by_component.has_key? key
resources_by_component[key].each do |res_id|
resources.delete res_id
end
resources_by_component.delete key
end
}
@statistics = Iowa::SessionStats.new(@pages)
end
def statistics
@statistics
end
# Handles the current request from the browser. Essentially all that
# is happening is thread synchronizations along with some tests to make
# sure that all the data necessary to handle the request exists.
# Then the call to handleRequest gets passed on to the object representing
# the current page.
def handleRequest(context)
@statistics.hit
@access_time = Time.now
mylog = Logger['iowa_log']
if @lock.locked?
raise "concurrent session access for #{self}"
end
@lock.synchronize do
begin
context.session = self
begin
requestPage = @currentPage
@context = context
raise IgnoreRequest unless context.requestID
return handle_resource(context) if @resources.has_key? "#{context.requestID}.#{context.actionID}"
raise PageExpired unless @pages.include? context.requestID
requestPage = @pages[context.requestID]
if @requestCount != context.requestID
requestPage.handleBacktrack
end
@currentPage = requestPage.dup
@application.reloadModified(@currentPage.class.to_s)
@currentPage.handleRequest(context)
nextPage = context.invokeAction
@currentPage = nextPage if nextPage
@requestCount = @requestCount.next
rescue IgnoreRequest
@currentPage = requestPage
end
handleResponse(context)
rescue PageExpired
expired(context)
end
end
end
# Invokes the handleResponse() method on the object representing the page
# and updates the page cache.
def handleResponse(context)
unless @currentPage
@currentPage = Component.pageNamed("Main", self)
end
@application.reloadModified(@currentPage.class.to_s,true)
context.requestID = @requestCount
@currentPage.handleResponse(context)
@pages[@requestCount] = @currentPage
end
# If one passes into resource_url content of some sort plus an optional
# content type (default is application/data if not specified), it will
# return a URL that can be used to access that content. If, on the
# other hand, one passes an arbitrary set of arguments and a block,
# a url will be returned that, when accesed, will cause the block to
# be executed. If the block returns an instance of Iowa::Resource,
# the content in the resource will be returned as a result of calling
# that URL, with the content type specified in the resource object.
# Any other return value will have that value returned as the result
# of calling the URL, with a content type of 'application/data'.
# The resources are tied to the page that created them, so they are
# available until the page expires. Once the page expires, the
# resource is deleted as well.
def resource_url(*args,&block)
if block_given?
resource = [block,args]
else
#args[0] == body, args[1] == content type
resource = Iowa::Resource.new(args[0], args[1])
end
resourceID = "r_#{@requestCount}.#{@resourceCount}"
@resources[resourceID] = resource
@resources_by_component[@context.requestID] ||= []
@resources_by_component[@context.requestID].push resourceID
@resourceCount = @resourceCount.next
return "#{@context.sessionURL}.#{resourceID}"
end
alias :resourceUrl :resource_url
def handle_resource(context)
r = @resources["#{context.requestID}.#{context.actionID}"]
resource = r.kind_of?(Array) ? r[0].call(*r[1]) : r
if resource.kind_of? Iowa::Resource
context.response << resource.body
context.request.content_type = resource.content_type
else
context.response << resource.to_s
context.request.content_type = 'application/data'
end
end
private
# Returns an explanation that the page being requested has expired, then
# issues a redirect to the head of the app. It'd be neat if there were
# some easy way to configure the contents of this page for an app.
def expired(context)
context.response << "<html><head><meta http-equiv=REFRESH content='#{@@expiryDelay}; URL=#{context.sessionURL}'></head><body><b>That page has expired.<p>You are being forwarded to your <a href='#{context.sessionURL}'>current point</a> in the session. Please continue from there.</b></body></html>"
end
end
end