#!/usr/bin/env ruby #-- # Copyright &169;2001-2008 Integrallis Software, LLC. # All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ require 'trellis/utils' require 'trellis/logging' require 'rubygems' require 'rack' require 'radius' require 'builder' require 'hpricot' require 'rexml/document' require 'extensions/string' require 'haml' require 'markaby' require 'redcloth' require 'bluecloth' require 'facets' require 'directory_watcher' require 'erubis' module Trellis # -- Application -- # Represents a Trellis Web Application. An application can define one or more # pages and it must define a home page or entry point into the application class Application include Logging include Rack::Utils # descendant application classes get a singleton class level instances for # holding homepage, dependent pages, static resource routing paths def self.inherited(child) #:nodoc: child.class_attr_reader(:homepage) child.attr_array(:static_routes) child.meta_def(:logger) { Application.logger } super end # class method that defines the homepage or entry point of the application # the entry point is the URL pattern / where the application is mounted def self.home(sym) @homepage = sym end # define url paths for static resources def self.map_static(urls = [], root = File.expand_path("#{File.dirname($0)}/../html/")) @static_routes << {:urls => urls, :root => root} end # bootstrap the application def start(port = 3000) Application.logger.info "Starting Trellis Application #{self.class} on port #{port}" directory_watcher = configure_directory_watcher directory_watcher.start Rack::Handler::Mongrel.run configured_instance, :Port => port do |server| trap(:INT) do Application.logger.info "Exiting Trellis Application #{self.class}" directory_watcher.stop server.stop end end rescue Exception => e Application.logger.warn "#{ e } (#{ e.class })!" end def configured_instance # configure rack middleware application = Rack::ShowStatus.new(self) application = Rack::ShowExceptions.new(application) application = Rack::Reloader.new(application) application = Rack::CommonLogger.new(application, Application.logger) application = Rack::Session::Cookie.new(application) # set all static resource paths self.class.static_routes.each do |path| application = Rack::Static.new(application, path) end application end # find the first page with a suitable router, if none is found use the default router def find_router_for(request) match = Page.subclasses.values.find { |page| page.router && page.router.matches?(request) } match ? match.router : DefaultRouter.new(:application => self) end # implements the rack specification def call(env) response = Rack::Response.new request = Rack::Request.new(env) Application.logger.debug "request received with url_root of #{request.script_name}" if request.script_name session = env['rack.session'] ||= {} router = find_router_for(request) route = router.route(request) page = route.destination.new if route.destination if page page.class.url_root = request.script_name page.path = request.path_info.sub(/^\//, '') page.inject_dependent_pages page.call_if_provided(:before_load) page.load_page_session_information(session) page.call_if_provided(:after_load) page.params = request.params.keys_to_symbols router.inject_parameters_into_page_instance(page, request) result = route.event ? page.process_event(route.event, route.value, route.source, session) : page Application.logger.debug "response is #{result} an instance of #{result.class}" # prepare the http response if (request.post? || route.event) && result.kind_of?(Trellis::Page) # for action events of posts then use redirect after post pattern # remove the events path and just return to the page path = result.path ? result.path.gsub(/\/events\/.*/, '') : result.class.class_to_sym response.status = 302 response.headers["Location"] = "#{request.script_name}/#{path}" Application.logger.debug "redirecting to ==> #{request.script_name}/#{path}" else # for render requests simply render the page response.body = result.kind_of?(Trellis::Page) ? result.render : result response.status = 200 end else response.status = 404 end response.finish end private def configure_directory_watcher(directory = nil) # set directory watcher to reload templates glob = [] Page::TEMPLATE_FORMATS.each do |format| glob << "*.#{format}" end templates_directory = directory || "#{File.dirname($0)}/../html/" directory_watcher = DirectoryWatcher.new templates_directory, :glob => glob, :pre_load => true directory_watcher.add_observer do |*args| args.each do |event| Application.logger.debug "directory watcher: #{event}" event_type = event.type.to_s if (event_type == 'modified' || event_type == 'stable') template = event.path format = File.extname(template).delete('.').to_sym page_locator = Page.template_registry[template] page = Page.get_page(page_locator) Application.logger.info "reloading template for page => #{page}: #{template}" File.open(template, "r") { |f| page.template(f.read, :format => format) } end end end Application.logger.info "watching #{templates_directory} for template changes..." directory_watcher end end # -- Route -- # A route object encapsulates an event, the event destination (target), the # event source and an optional event value class Route attr_reader :destination, :event, :source, :value def initialize(destination, event = nil, source = nil, value = nil) @destination, @event, @source, @value = destination, event, source, value end end # -- Router -- # A Router returns a Route in response to an HTTP request class Router EVENT_REGEX = %r{^(?:.+)/events/(?:([^/\.]+)(?:\.([^/\.]+)?)?)(?:/(?:([^\.]+)?))?} attr_reader :application, :pattern, :keys, :path, :page def initialize(options={}) @application = options[:application] @path = options[:path] @page = options[:page] compile_path if @path end def route(request = nil) # get the event information if any value, source, event = request.path_info.match(EVENT_REGEX).to_a.reverse if request Route.new(@page, event, source, value) end def matches?(request) request.path_info.gsub(/\/events\/.*/, '').match(@pattern) != nil end def inject_parameters_into_page_instance(page, request) # extract parameters and named parameters from request if @pattern && @page && match = @pattern.match(request.path_info.gsub(/\/events\/.*/, '')) values = match.captures.to_a params = if @keys.any? @keys.zip(values).inject({}) do |hash,(k,v)| if k == 'splat' (hash[k] ||= []) << v else hash[k] = v end hash end elsif values.any? {'captures' => values} else {} end params << request.params params.each_pair { |name, value| page.instance_variable_set("@#{name}".to_sym, value) } end end private # borrowed (stolen) from Sinatra! def compile_path @keys = [] if @path.respond_to? :to_str special_chars = %w{. + ( )} pattern = @path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match| case match when "*" @keys << 'splat' "(.*?)" when *special_chars Regexp.escape(match) else @keys << $2[1..-1] "([^/?]+)" end end @pattern = /^#{pattern}$/ elsif @path.respond_to?(:keys) && @path.respond_to?(:match) @pattern = @path @keys = @path.keys elsif @path.respond_to? :match @pattern = path else raise TypeError, @path end end end # -- DefaultRouter -- # The default routing scheme is in the form /page[.event[_source]][/value][?query_params] class DefaultRouter < Router ROUTE_REGEX = %r{^/([^/]+)(?:/(?:events/(?:([^/\.]+)(?:\.([^/\.]+)?)?)(?:/(?:([^\.]+)?))?)?)?} def route(request) value, source, event, destination = request.path_info.match(ROUTE_REGEX).to_a.reverse destination = @application.class.homepage unless destination page = Page.get_page(destination.to_sym) Route.new(page, event, source, value) end def matches?(request) request.path_info.match(ROUTE_REGEX) != nil end def self.to_uri(options={}) url_root = options[:url_root] page = options[:page] event = options[:event] source = options[:source] value = options[:value] destination = page.kind_of?(Trellis::Page) ? (page.path || page.class.class_to_sym) : page url_root = page.kind_of?(Trellis::Page) && page.class.url_root ? "/#{page.class.url_root}" : '/' unless url_root source = source ? ".#{source}" : '' value = value ? "/#{value}" : '' event_info = event ? "/events/#{event}#{source}#{value}" : '' "#{url_root}#{destination}#{event_info}" end end # -- Page -- # Represents a Web Page in a Trellis Application. A Page can contain multiple # components and it defines a template or view either as an external file # (xml, xhtml, other) or programmatically using Markaby or HAML # A Trellis Page contains listener methods to respond to events trigger by # components in the same page or other pages class Page TEMPLATE_FORMATS = [:html, :xhtml, :haml, :textile, :markdown, :eruby] @@subclasses = Hash.new @@template_registry = Hash.new attr_accessor :params, :path, :logger def self.inherited(child) #:nodoc: sym = child.class_to_sym @@subclasses[sym] = child if sym child.instance_variable_set(:@name, child.underscore_class_name) child.attr_array(:pages, :create_accessor => false) child.attr_array(:components) child.attr_array(:stateful_components) child.attr_array(:persistents) child.class_attr_accessor :url_root child.class_attr_accessor :name child.class_attr_accessor :router child.class_attr_accessor :layout child.meta_def(:add_stateful_component) { |tid,clazz| @stateful_components << [tid,clazz] } locate_template child super end def self.template(body = nil, options = nil, &block) @format = (options[:format] if options) || :html if block_given? mab = Markaby::Builder.new({}, self, &block) html = mab.to_s else case @format when :haml html = Haml::Engine.new(body).render when :textile html = RedCloth.new(body).to_html when :markdown html = "
#{BlueCloth.new(body).to_html}" else # assume the body is (x)html, also eruby is treated as (x)html at this point html = body end end @template = Hpricot.XML(html) find_components end def self.parsed_template # try to reload the template if it wasn't found on during inherited # since it could have failed if the app was not mounted as root unless @template Application.logger.debug "parsed template was no loaded, attempting to load..." locate_template(self) end @template end def self.format @format end def self.pages(*syms) @pages = @pages | syms end def self.route(path) router = Router.new(:path => path, :page => self) self.instance_variable_set(:@router, router) end def self.persistent(*fields) instance_attr_accessor fields @persistents = @persistents | fields end def self.get_page(sym) @@subclasses[sym] end def self.subclasses @@subclasses end def initialize # TODO this is Ugly.... should no do it in initialize since it'll require super in child classes self.class.stateful_components.each do |id_component| id_component[1].enhance_page(self, id_component[0]) end @logger = Application.logger end def process_event(event, value, source, session) method = source ? "on_#{event}_from_#{source}" : "on_#{event}" # execute the method passing the value if necessary unless value method_result = send method.to_sym else method_result = send method.to_sym, Rack::Utils.unescape(value) end # determine navigation flow based on the return value of the method call if method_result if method_result.kind_of?(Trellis::Page) page = method_result # save the current page persistent information if self != method_result save_page_session_information(session) page.inject_dependent_pages page.call_if_provided(:before_load) end # save the persistent information before rendering a response page.save_page_session_information(session) end end method_result end def load_page_session_information(session) load_persistent_fields_data(session) load_stateful_components_data(session) end def save_page_session_information(session) save_persistent_fields_data(session) save_stateful_components_data(session) end def render call_if_provided(:before_render) result = Renderer.new(self).render call_if_provided(:after_render) result end # inject an instance of each of the injected pages classes as instance variables # of the current page def inject_dependent_pages self.class.inject_dependent_pages(self) end def self.inject_dependent_pages(target) @pages.each do |sym| clazz = Page.get_page(sym) # if the injected page class is not found # throw an exception in production mode or # dynamically generate a page in development mode unless clazz target_class = sym.to_s.camelcase(:upper) Application.logger.debug "creating stand in page class #{target_class} for symbol #{sym}" clazz = Page.create_child(target_class) do def method_missing(sym, *args, &block) Application.logger.debug "faking response to #{sym}(#{args}) from #{self} an instance of #{self.class}" self end template do xhtml_strict { head { title "Stand-in Page" } body { h1 { text %[Stand-in Page for