require 'thread'
require 'log4r'
include Log4r
require 'iowa/ISAAC'
require 'iowa/SessionStore'
require 'iowa/ApplicationStats'
module Iowa
# Application sits at the top of everything. This class represents an
# entire Iowa application and handles the things relevant to the application
# as a whole.
class Application
attr_accessor :mapfile
begin
BindingCommentsRegexp
rescue
BindingCommentsRegexp = /^\s*#.*$/
end
@@applicationClass = Application
# This is the default number of sessions to cache within the
# Application.
# Down the road, it'd be nice to have the option to make this
# dynamic, so that the number of cached sessions would fluctuate
# depending on the amount of usage that the application was seeing.
@@cachedSessions = 10
@@cacheTTL = nil
# Returns a new instance of Application
def Application.newApplication(*args)
mylog = Logger['iowa_log']
@@applicationClass.new(*args)
end
# Allows one to specify a subclass to be used when creating new
# objects with newApplication().
def Application.inherited(subclass)
@@applicationClass = subclass
end
def Application.cachedSessions
@@cachedSessions
end
def Application.cachedSessions=(val)
@@cachedSessions = val
end
def Application.cacheTTL
@@cacheTTL
end
def Application.cacheTTL=(val)
@cacheTTL = val
end
# Performs all of the work necessary to start a new Iowa Application.
def initialize(docroot)
mylog = Logger['iowa_log']
@docroot = docroot
@sessions = SessionStore.new(@@cachedSessions,@@cacheTTL)
@templateCache = {}
@templateMTimes = {}
@sessionLock = Mutex.new
@templateLock = Mutex.new
@random_numbers = Crypt::ISAAC.new
@statistics = Iowa::ApplicationStats.new(@sessions)
@reload_scan_mode = 'singular'
$iowa_application = self
mylog.info " Application with docroot of #{docroot} initialized."
nil
end
def statistics
@statistics
end
def reload_scan_mode
@reload_scan_mode
end
# Set the reload scanning mode. The two current settings are 'singular'
# and 'plural'.
# singular: only check the named template for loading. This is much
# more efficient for large applications as it eliminates making a stat()
# system call on each file for every single request. Most of the time
# this should be sufficient for detecting any changes that have been
# made and reloading them. If, in some case, it is found not to be
# sufficient, one can set the mode to 'plural' which is the old
# normal mode of operation. 'singular' is the default mode of
# operation.
# plural: checks the mtime of every file in the iowa docroot directory
# every time a request is handled. This is guaranteed to detect
# and reload changes in every file. For sites that have a lot of
# files in their docroot, though, this is a performance hit.
def reload_scan_mode=(mode)
if mode == 'singular'
@reload_scan_mode = 'singular'
else
@reload_scan_mode = 'plural'
end
end
# handleRequest() is called whenever a new request is made to the application.
def handleRequest(context)
session = nil
exception = nil
@statistics.hit
mylog = Logger['iowa_log']
@sessionLock.synchronize do
unless context.sessionID.to_s != ''
context.sessionID = randomSession
begin
@sessions[context.sessionID] = Session.newSession
@sessions[context.sessionID].application = self
rescue Exception => exception
end
end
begin
session = @sessions[context.sessionID] unless exception
rescue Exception => exception
end
end
if exception
throw :session_error,exception
end
if session
session.handleRequest(context)
else
begin
invalidSession(context)
rescue Exception => exception
end
if exception
throw :session_error,exception
end
end
end
# reload both the an optional .iwa file and the corresponding
# .html file if the mtimes on either is later than the
# mtime in the cache
# This curently scans every file in the directory every single
# time a request is handled. For a large app with a lot of
# files, this wastefully burns a lot of time. So, the exact
# behavior of the scan is going to become selectable. The
# code will provide, in addition to this "scan everything"
# approach, the option to only look at the file for the class
# currently being executed. Most of the time, this is all
# that is needed, and when it is less than is needed, one
# can tell the application to switch to the other mode.
def reloadModified(component_class=nil,only_if_singular=nil,import_call=false)
mylog = Logger['iowa_log']
pathlist = nil
classname = component_class.to_s.split('::').pop.to_s
if (reload_scan_mode == 'singular' and classname != nil)
pathlist = [pathForName(component_class)]
elsif (reload_scan_mode != 'singular' and !only_if_singular)
fileRegex = Regexp.new('htm|html$')
# Using a proc here because, for some reason, I don't want to use
# a method. Probably stupid.
search_proc = proc do |dirpath|
r = []
Dir.foreach(dirpath) do |filename|
next if filename == '.' or filename == '..'
fullname = "#{dirpath}/#{filename}"
if FileTest.directory? fullname
r.concat(search_proc.call(fullname))
elsif fileRegex.match(filename)
r.push fullname.gsub(/\/\//,'/')
end
end
r
end
pathlist = search_proc.call(@docroot)
end
if pathlist
if import_call
reloadLoop(pathlist)
else
@templateLock.synchronize do
reloadLoop(pathlist)
end
end
end
end
def reloadLoop(pathlist)
mylog = Logger['iowa_log']
pathlist.each do |path|
file = File.new(path)
fileMtime = file.mtime
mtime = @templateMTimes[path]
# Check the mtime of the iwa file.
iwaPath = path.sub(/\.html$/, '.iwa')
if File.exist?(iwaPath)
iwaMtime = File.stat(iwaPath).mtime
fileMtime = iwaMtime if iwaMtime > fileMtime
end
# Check the mtime of the bnd file.
bndPath = path.sub(/\.html$/,'.bnd')
if File.exist?(bndPath)
bndMtime = File.stat(bndPath).mtime
fileMtime = bndMtime if bndMtime > fileMtime
end
unless mtime and mtime >= fileMtime
mylog.info "Loading template #{path}"
reload file
@templateMTimes[path] = fileMtime
end
end
end
def initialLoad
mylog = Logger['iowa_log']
fileRegex = Regexp.new('htm|html$')
search_proc = proc do |dirpath|
r = []
Dir.foreach(dirpath) do |filename|
next if filename == '.' or filename == '..'
fullname = "#{dirpath}/#{filename}"
if FileTest.directory? fullname
r.concat(search_proc.call(fullname))
elsif fileRegex.match(filename)
r.push fullname
end
end
r.sort
end
pathlist = search_proc.call(@docroot)
reloadLoop(pathlist)
end
# Returns the template that implements the named component.
def templateForComponent(name)
template = nil
@templateLock.synchronize do
template = @templateCache[pathForName(name)]
end
template
end
# import is used to make other templates available within the
# context of the template where it is called.
def import(name)
mylog = Logger['iowa_log']
mylog.info " Importing #{name} with reload mode #{reload_scan_mode}..."
# Deal with recursion. If A imports B and B imports A, we
# will get stuck.
if reload_scan_mode == 'singular'
reloadModified(name,true,true)
else
reload File.new(pathForName(name))
end
mylog.info " Done importing."
end
# Everything from here down is private.
private
def pathForName(name)
name.gsub!(/Iowa::Application::Content_Classes::/,'')
my_docroot = @docroot
my_docroot << '/' unless my_docroot[-1,1] == '/'
my_docroot + name.gsub(/::/,'/') + ".html"
end
# Extract the code and bindings from the code data.
def get_code_and_bindings(codedata)
codedata.sub!(/<\?(.*?)\?>/m, "")
bindings = ''
bindings = $1.gsub(BindingCommentsRegexp,'') if $1
if m = /<%(.*?)%>/m.match(codedata)
code = m[1]
else
code = codedata
end
[code,bindings]
end
# This method loads (or reloads) a template file.
# To completely separate the layout (template) from the code,
# put the HTML in a file Foo.html, and then place the code for
# that template into another file, Foo.iwa.
def reload(file)
mylog = Logger['iowa_log']
data = file.read
code_text = ''
bindings_text = ''
# Read the codefile and check for bindings embedded into it.
codefile_path = file.path.sub('html','iwa')
if FileTest.exist?(codefile_path) and FileTest.readable?(codefile_path)
File.open(codefile_path) do |codefile|
codedata = codefile.read.gsub(/\cM/,'')
code_text, bindings_text = *get_code_and_bindings(codedata)
end
else
codedata = data.gsub(/\cM/,'')
unless /<%.*?%>/m.match(codedata)
codedata = defaultScript(file).gsub(/\cM/,'')
code_text, bindings_text = *get_code_and_bindings(codedata)
else
codedata.sub!(/<%(.*?)%>/m, "")
code_text = $1
codedata.sub!(/<\?(.*?)\?>/m, "")
bindings_text = $1.gsub(BindingCommentsRegexp,'') if $1
data = codedata
end
end
# Now check for a dedicated bindings file.
# In a dedicated bindings file, one can have multiple bindings
# blocks with comments (lines with a first non-whitespace character
# of #) anywhere. This is just a feature to allow for some intelligent
# organization of bindings and comments, if desired.
bindingfile_path = file.path.sub('html','bnd')
if FileTest.exist?(bindingfile_path) and FileTest.readable?(bindingfile_path)
File.open(bindingfile_path) do |bindingfile|
bindingdata = bindingfile.read.gsub(/\cM/,'')
bindingdata = bindingdata.gsub(/<\?/,'').gsub(/\?>/,'').gsub(BindingCommentsRegexp,'') if bindingdata
bindings_text << bindingdata
end
end
# There's a bit of magic that has to occur, now. In order to
# support the notion of the subdirectory that the script file
# is found in relating to the namespace (using modules as
# namespace) for the component, reload() needs to figure out
# just what the namespace is supposed to be for the file, then
# check to see if that namespace has a already been created,
# create it if it has not, and finally eval the script file
# content within the context of the namespace (module).
# First, what's the namespace? Basically, we subtract the
# Iowa docroot from the file path and see what is left.
my_docroot = @docroot
my_docroot << '/' unless my_docroot[-1,1] == '/'
script_namespace_parts = file.path.gsub(/^#{my_docroot}/,'').split('/')
script_namespace_parts.pop
script_namespace_parts.delete('.')
script_namespace_parts.delete('..')
script_namespace_parts.unshift('Content_Classes')
script_namespace = (['Iowa::Application'] + script_namespace_parts).join('::')
pre = ''
post = ''
# import() is defined as a method of Iowa::Application. However,
# it needs to be available in the other contexts so that imports work.
# So, we do a little bit of magic here to make that happen.
script_namespace_parts.each do |sym|
pre << "module #{sym}; def #{sym}.import(f); $iowa_application.import(f);end;"
post << "end;"
end
mylog.info "Creating namespace #{script_namespace}"
eval(pre + post)
eval script_namespace
begin
# Now execute our code within the namespace.
/(.*)/m.match(code_text)
eval_code = "#{script_namespace}.module_eval \$1"
eval eval_code
bindings = BindingsParser.new(bindings_text ? bindings_text : '').bindings
mylog.info "Stuffing #{file.path} into template cache"
@templateCache[file.path] = TemplateParser.new(data, bindings).root
rescue Exception => e
mylog.info "There was an error while processing #{file.path}:\n#{e.to_s}\n#{e.backtrace}\n"
end
end
# If there is only an HTML file, without either an embedded code section
# or a corresponding .iwa file, then we apply a basic default.
# If a file, 'DefaultScriptFile.iwa', exists in the Iowa docroot, the
# contents of that file will be used for the default script. That file
# should be written as a standard Iowa .iwa file, with one exception.
# The exception is that the name of the class should be written as:
# [--CLASSNAME--]
# This placeholder will be expanded by Iowa into the actual name of the
# class to be created.
def defaultScript(file)
mylog = Logger['iowa_log']
name = /.*\/(.+)\.html/.match(file.path)[1]
r = ''
my_docroot = @docroot
my_docroot << '/' unless my_docroot[-1,1] == '/'
my_docroot_minus_slash = my_docroot.sub(/\/$/,'')
filepath = file.path
filepath.sub!(/#{my_docroot}/,'')
path_parts = filepath.split('/')
# Knock the filename off of the array.
path_parts.pop
# And put the docroot path onto the beginning of the array.
path_parts.unshift my_docroot_minus_slash
df_found = nil
search_path = ''
while path_parts.length > 0
search_path << "#{path_parts.shift}/"
default_script_file = search_path + 'DefaultScriptFile.iwa'
default_script_file.gsub(/\/\//,'/')
if FileTest.exists? default_script_file
df_found = default_script_file
end
end
if df_found
File.open(df_found,'r') do |fh|
fh.each {|line| r << line}
end
mylog.info "Loaded #{df_found} for script file for #{file.path}"
else
r << "<%class [--CLASSNAME--] < Iowa::Component; end%>"
mylog.info "Used basic scriptfile for #{file.path}"
end
r.gsub!(/\[--CLASSNAME--\]/,name)
r
end
# Generate the unique session ID. It is comprised of two large random
# integers expressed in hexadecimal plus the application of the crypt
# function on the floating point representation of the current time.
def randomSession
r1 = [@random_numbers.rand(2147483647)].pack('L').unpack('h20')[0].reverse
r2 = [@random_numbers.rand(2147483647)].pack('L').unpack('h20')[0].reverse
t = ("%10.6f" % Time.now.to_f).to_s.gsub(/\./,'').reverse.crypt('ab').gsub(/[.\/]/, "")
id = "#{r1}-#{r2}-#{t}"
@sessions.include?(id) ? randomSession : id
end
# Make this something that can be configured.
def invalidSession(context)
context.response << "<html><head><meta http-equiv=REFRESH content='1; URL=#{context.baseURL}'></head><body><b>That session no longer exists.<p>You are being forwarded to a <a href='#{context.baseURL}'>new session</a>.</b></body></html>"
end
end
end