require 'thread' require 'iowa/Hash' require 'iowa/Request' require 'iowa/KeyValueCoding' require 'iowa/caches/LRUCache' module Iowa module Dispatchers class StandardDispatcher attr_accessor :mapfile class NoMapFile < Exception; end # {:mapfile => 'FILENAME', :poll_interval => 30, :map => {'/foo/bar.html' => 'Bar'}, :rewrites => [], :secondary_rewrites => []} def initialize(*args) @mutex = Mutex.new @mapfileMTime = Time.at(1) @next_check = Time.at(1) if args[0].respond_to?(:[]) if args[0].kind_of?(::Hash) oargs = args[0] args = Iowa::Hash.new args.step_merge!(oargs) end args.stringify_keys! if args.respond_to? :stringify_keys! @mapfile = args[Cmapfile] @poll_interval = args['poll_interval'] ? args['poll_interval'].to_i : 30 @path_map = args['map'] || {} @rewrites = RuleSet.new(args['rewrites'] || []) @secondary_rewrites = RuleSet.new(args['secondary_rewrites'] || []) else @mapfile = args[0] @poll_interval = args[1] ? args[1].to_i : 30 @path_map = args[2] || {} @rewrites = RuleSet.new(args[3] || []) @secondary_rewrites = RuleSet.new(args[4] || []) end raise(NoMapFile, "The mapfile (#{@mapfile}) does not appear to exist.") if !FileTest.exist?(@mapfile.to_s) if @mapfile @mapfile ||= 'mapfile.map' if FileTest.exist?('mapfile.map') check_mapfile @mapfile end # Checks to see if the mapfile needs to be reloaded. To avoid # a deluge of stat() calls, it will only check once every # @poll_interval seconds (set during initialization; defaults to # 30 seconds). def check_mapfile if @mapfile and @next_check < Time.now @mutex.synchronize do @next_check = Time.at(Time.now + @poll_interval) if File.stat(@mapfile).mtime != @mapfileMTime load_mapfile end end end end # Loads @mapfile. def load_mapfile raw_data = {} File.open(@mapfile) do |mf| rd = YAML::load(mf) break unless rd.respond_to?(:has_key?) raw_data = Iowa::Hash.new raw_data.step_merge!(rd) raw_data.symbolify_keys! rs = raw_data.has_key?(:rewrites) ? raw_data[:rewrites] : [] @rewrites = RuleSet.new(rs) rs = (raw_data.respond_to?(:[]) and raw_data.has_key?(:secondary_rewrites)) ? raw_data[:secondary_rewrites] : [] @secondary_rewrites = RuleSet.new(rs) @path_map = (raw_data.respond_to?(:[]) and raw_data.has_key?(:map)) ? raw_data[:map] : {} if raw_data.respond_to?(:delete) raw_data.delete(:rewrites) raw_data.delete(:secondary_rewrites) raw_data.delete(:map) end raw_data.each {|k,v| @path_map[k] = v if (k.class.is_a?(String) and v.class.is_a?(String))} @mapfileMTime = mf.mtime end end # Returns true if the dispatcher is willing to handle this request. def handleRequest?(request) check_mapfile if !Iowa.app.policy.getIDs(request.uri)[1].nil? true else process(request) end end # Dispatches the request. def dispatch(session,context,dispatch_destination = nil) unless dispatch_destination or context.actionID request = context.request check_mapfile dispatch_destination = process(request) end session.handleRequest(context,dispatch_destination) end def process(request) dd = @rewrites.process(request) return dd if dd.is_a?(Iowa::DispatchDestination) if c = @path_map[request.uri] return Iowa::DispatchDestination.new(c) else dd = @secondary_rewrites.process(request) return dd if dd.is_a?(Iowa::DispatchDestination) # At this point, the easy checks are done. If we are still here, there was no # match in the mapfile, and no rule provided a specific dispatch destination. # So, use a set of heuristics, assuming a RESTfully structured URI, to figure # out what should be done. # # The basic pattern that is expected is /component/method/arg1/arg2/argn # # On top of this basic pattern, there are some specific exceptions, based on # the HTTP verb the request is using (GET, PUT, POST, DELETE), that # deviate from that basic pattern. These provide shortcuts for RESTful URLs, # and are based in part on the RESTful Rails work. # # Method dispatch also supports the notion of dispatching to verb specific # methods. This is ultimately carried out in the Context object when an # action is being invoked, however, as the target object must be interrogated # to determine whether it has a method specific to the verb. # dd = nil request_uri = request.uri.sub(/\.\w+$/,C_empty) #/component/method/arg1/arg2/argn if request_uri =~ /^\/(\w+)\/(?:(\w+)((?:\/\w+)+)|([a-zA-Z]\w*)((?:\/\w+)*))$/ c = $1 m = $2 || $4 args = $3 || $5 args = args ? args[1..-1].to_s.split(C_slash) : [] dd = Iowa::DispatchDestination.new(c,m,args) #/component/arg1/arg2/argn;method #elsif request_uri =~ /^\/(\w+)((?:\/[\d\-a-f]+)+);(\w+)$/ elsif request_uri =~ /^\/(\w+)((?:\/[\w%]+)+);(\w+)$/ c = $1 m = $3 args = $2[1..-1].to_s.split(C_slash) dd = Iowa::DispatchDestination.new(c,m,args) else request.request_method = request.params[C_method] if request.params[C_method] rmeth = request.params[C_method] ? request.params[C_method].upcase : request.request_method.upcase if rmeth == CGET or rmeth == CHEAD if request_uri =~ /^\/(\w+)$/ dd = Iowa::DispatchDestination.new($1) #elsif request_uri =~ /^\/(\w+)((?:\/[\d\-a-f]+)+)$/ elsif request_uri =~ /^\/(\w+)((?:\/[\w%]+)+)$/ c = $1 args = $2[1..-1].to_s.split(C_slash) dd = Iowa::DispatchDestination.new(c,Cshow,args) end elsif rmeth == CPOST if request_uri =~ /^\/(\w+)$/ dd = Iowa::DispatchDestination.new($1,Ccreate) end elsif rmeth == CPUT #if request_uri =~ /^\/(\w+)((?:\/[\d\-a-f]+)+)$/ if request_uri =~ /^\/(\w+)((?:\/[\w%]+)+)$/ c = $1 args = $2[1..-1].to_s.split(C_slash) dd = Iowa::DispatchDestination.new(c,Cupdate,args) end elsif rmeth == CDELETE #if request_uri =~ /^\/(\w+)((?:\/[\d\-a-f]+)+)$/ if request_uri =~ /^\/(\w+)((?:\/[\w%]+)+)$/ c = $1 args = $2[1..-1].to_s.split(C_slash) dd = Iowa::DispatchDestination.new(c,Cdestroy,args) end end end dd end end # -------------------- class RuleSet def initialize(rule_struct) @rules = [] if rule_struct.respond_to?(:[]) if rule_struct.respond_to?(:has_key?) @rules << RewriteRule.new(rule_struct) else rule_struct.each {|rule| @rules << RewriteRule.new(rule)} end end end def process(request) catch(:final) do @rules.each {|rule| rule.process(request)} end end end class RewriteRule def initialize(prearg = {:match => '', :sub => nil, :target => nil, :gsub => nil, :call => nil, :eval => nil, :branch => nil, :dispatch => nil, :final => false, :cache => nil}) arg = Iowa::Hash.new arg.step_merge!(prearg) arg.symbolify_keys! if arg[:cache] @cache_size = arg[:cache].to_i > 0 ? arg[:cache].to_i : 100 self.cacheable = true else @cache_size = 100 self.cacheable = false end @final = arg[:final] ? true : false if !arg[:match].is_a?(Regexp) and arg[:match] =~ /^\s*(\{|do).*(\}|end)\s*/m instance_eval("@match = Proc.new #{arg[:match]}") else @match = arg[:match].is_a?(Regexp) ? arg[:match] : Regexp.new(arg[:match]) end if arg[:target] =~ /^\s*(\{|do).*(\}|end)\s*/m instance_eval("@target = Proc.new #{arg[:target]}") else @target = arg[:target] end @subtype = (arg[:gsub] && :gsub) || (arg[:sub] && :sub) || nil rawsub = arg[:gsub] || arg[:sub] if rawsub =~ /^\s*(\{|do).*(\}|end)\s*/m instance_eval("@subproc = Proc.new #{rawsub}") @subsub = nil else @subsub = rawsub @subproc = nil end if arg[:eval] ep = arg[:eval] if ep !~ /^\s*(\{|do).*(\}|end)\s*/m ep = "{|request| #{ep}}" end instance_eval("@evalproc = Proc.new #{ep}") else @evalproc = nil end @call = arg[:call] @branch = arg[:branch] ? RuleSet.new(arg[:branch]) : nil if arg[:dispatch] =~ /^\s*(\{|do).*(\}|end)\s*/m instance_eval("@dispatch = Proc.new #{arg[:dispatch]}") @final = true elsif arg[:dispatch] @dispatch = Iowa::DispatchDestination.new(arg[:dispatch]) @final = true else @dispatch = nil end end def cacheable? @cacheable end def cacheable=(val) if val @cache = Iowa::Caches::LRUCache(@cache_size) @cacheable = true else @cache = nil @cacheable = false end end def component=(c) if @destination @destination.component = c else @destination = Iowa::DispatchDestination.new(c) end end def component @destination ? @destination.component : nil end def method=(m) if @destination @destination.method = m else @destination = Iowa::DispatchDestination.new(m) end end def method @destination ? @destination.method : nil end def args=(a) if @destination @destination.args = a else @destination = Iowa::DispatchDestination.new(a) end end def args @destination ? @destination.args : nil end def process(request) do_branch = @branch original_uri = request.uri request.uri = ::String.new(request.uri) if request.uri.frozen? @destination = nil if @cacheable and @cache.include?(request.uri) request.uri = @cache[request.uri] do_branch = false elsif (@match.is_a?(::Proc) ? @match.call(request) : @match.match(request.uri)) if @subtype if @subsub if @subtype == :sub request.uri = request.uri.sub(@match,@subsub) elsif @subtype == :gsub request.uri = request.uri.gsub(@match,@subsub) end elsif @subproc if @subtype == :sub request.uri = request.uri.sub(@match) {|*args| @subproc.call(request,*args)} elsif @subtype == :gsub request.uri = request.uri.gsub(@match) {|*args| @subproc.call(request,*args)} end end elsif @call call_completed = false if @call =~ /(?:::|^[A-Z][\w\d]*\.)/ @call = @call.sub(/^::/,'') if @call =~ /::/ parts = @call.split(/::/).reject {|p| p == ''} else parts = @call.split('.',2) end meth = parts.pop catch(:bad_const) do klass = parts.inject(Object) do |o,n| if o.const_defined? n o.const_get n else throw :bad_const end end throw :bad_const unless klass.existsKeyPath?(meth) klass.valueForKeyPathWithArgs(meth,request) call_completed = true end end unless call_completed ::TOPLEVEL_BINDING.send(:valueForKeyPathWithArgs,@call,request) end elsif @evalproc @evalproc.call(request) end if do_branch and !@final @branch.process(request) end @destination = @dispatch @cache[original_uri] = request.uri if @cacheable throw(:final,@destination) if @final end @destination end end end end end