require 'sproutcore/jsdoc' require 'net/http' require 'uri' module SproutCore module Merb # A subclass of this controller handles all incoming requests for the # location it is mounted at. For index.html requests, it will rebuild the # html file everytime it is requested if you are in development mode. For # all other requests, it will build the resource one time and then return # the file if it already exists. class BundleController < ::Merb::Controller def self.library_for_class(klass) (@registered_libraries ||= {})[klass] end def self.register_library_for_class(library, klass) (@registered_libraries ||= {})[klass] = library end # Entry point for all requests routed through the SproutCore controller. # Example the request path to determine which bundle should handle the # request. def main self.reset_current_bundle puts current_bundle # Before we do anything, set the build_mode for the bundles. This # shouldn't change during execution, but if we set this during the # router call, the Merb.environment is sometimes not ready yet. # if ::Merb.environment.to_sym == :production Bundle.build_mode = :production else ::SproutCore.logger.level = Logger::DEBUG end # Make sure we can service this with a bundle # If no bundle is found, try to proxy... if current_bundle.nil? # if proxy url, return proxy... url = request.uri proxy_url, proxy_opts = library.proxy_url_for(url) if proxy_url unless request.query_string.length == 0 proxy_url = proxy_url + "?" + request.query_string end return handle_proxy(url, proxy_url, proxy_opts) else raise(NotFound, "No SproutCore Bundle registered at this location.") end end # Check for a few special urls that need to be rewritten url = request.path if request.method == :get url = rewrite_bundle_if(url, /^#{current_bundle.index_root}\/-tests/, :sc_test_runner) url = rewrite_bundle_if(url, /^#{current_bundle.index_root}\/-docs/, :sc_docs) end # If we are in development mode, reload bundle first library.reload_bundles! if current_bundle.build_mode == :development # Get the normalized URL for the requested resource url = current_bundle.normalize_url(url) # Check for a few special urls for built-in services and route them off case url when "#{current_bundle.url_root}/-tests/index.js" ret = handle_test(url) when "#{current_bundle.index_root}/-docs/index.html" ret = (request.method == :post) ? handle_doc(url) : handle_resource(url) when "#{current_bundle.url_root}/-docs/index.html" ret = (request.method == :post) ? handle_doc(url) : handle_resource(url) else ret = handle_resource(url) end # Done! return ret end # Invoked whenever you request a regular resource def handle_resource(url) # Get the entry for the resource. entry = current_bundle.entry_for_url(url, :hidden => :include) raise(NotFound, "No matching entry in #{current_bundle.bundle_name} for #{url}") if entry.nil? build_path = entry.build_path # Found an entry, build the resource. If the resource has already # been built, this will not do much. If this the resource is an # index.html file, force the build. is_index = /\/index\.html$/ =~ url # If we need to serve the source directly, then just set the # build path to the source_path. if entry.use_source_directly? build_path = entry.source_path # Otherwise, run the build command on the entry to make sure the # file is up to date. else current_bundle.build_entry(entry, :force => is_index, :hidden => :include) end # Move to final build path if necessary if (build_path != entry.build_path) && File.exists?(entry.build_path) FileUtils.mv(entry.build_path, build_path) end # And return the file. Set the content type using a mime-map borroed # from Rack. headers['Content-Type'] = entry.content_type headers['Content-Length'] = File.size(build_path).to_s ret = File.open(build_path, 'rb') # In development mode only, immediately delete built composite # resources. We want each request to come directly to us. if (current_bundle.build_mode == :development) && (!entry.cacheable?) # Deleting composite resources will not work in windows because it # does not like to have files you just open deleted. (Its OK on # windows) FileUtils.rm(build_path) if (RUBY_PLATFORM !~ /mswin32/) end return ret end # Proxy the request and return the result... def handle_proxy(url, proxy_url, opts ={}) # collect the method (don't use request.method as that might unmasquerade delete and put requests) http_method = request.env['REQUEST_METHOD'].to_s.downcase # capture the origin host for cookies. strip away any port. origin_host = request.host.gsub(/:[0-9]+$/,'') # collect the headers... headers = {} request.env.each do |key, value| next unless key =~ /^HTTP_/ key = key.gsub(/^HTTP_/,'').titleize.gsub(' ','-') headers[key] = value end # add the Content-Type header if(request.content_type) headers['Content-Type'] = request.content_type; SC.logger.debug "Content-Type: #{headers['Content-Type']}" end uri = URI.parse(proxy_url) http_host = uri.host http_port = uri.port http_path = [uri.path, uri.query].compact.join('?') http_path = '/' if http_path.nil? || http_path.size <= 0 # now make the request... response = nil # Handle those that require a body. no_body_method = %w(delete get copy head move options trace) ::Net::HTTP.start(http_host, http_port) do |http| if no_body_method.include?(http_method) response = http.send(http_method, http_path, headers) else http_body = request.raw_post response = http.send(http_method, http_path, http_body, headers) end end # Now set the status, headers, and body. @status = response.code SC.logger.debug " ~ PROXY: #{@status} #{request.uri} -> http://#{http_host}:#{http_port}#{http_path}" # Transfer response headers into reponse ignore = ['transfer-encoding', 'keep-alive', 'connection'] response.each do | key, value | next if ignore.include?(key.downcase) # If this is a cookie, strip out the domain. This technically may # break certain scenarios where services try to set cross-domain # cookies, but those services should not be doing that anyway... if key.downcase == 'set-cookie' value.gsub!(/domain=[^\;]+\;? ?/,'') end # Location headers should rewrite the hostname if it is included. if key.downcase == 'location' value.gsub!(/^http:\/\/#{http_host}(:[0-9]+)?\//, "http://#{request.host}/") end # Prep key and set header. key = key.split('-').map { |x| x.downcase.capitalize }.join('-') SC.logger.debug " #{key}: #{value}" @headers[key] = value end SC.logger.debug '' # Transfer response body return response.body end # Returns JSON containing all of the tests def handle_test(url) test_entries = current_bundle.entries_for(:test, :hidden => :include) content_type = :json ret = test_entries.map do |entry| { :name => entry.filename.gsub(/^tests\//,''), :url => "#{entry.url}?#{entry.timestamp}" } end return ret.to_json end # If you POST to this URL, regenerates the docs. def handle_doc(url) JSDoc.generate :bundle => current_bundle return "OK" end ###################################################################### ## Support Methods ## # Returns the library for this class def library ::SproutCore::Merb::BundleController.library_for_class(self.class) end # Returns the bundle for this request def current_bundle return @current_bundle unless @current_bundle.nil? # Tear down the URL, looking for the first bundle bundle_map = library.bundles_grouped_by_url url = request.path.split('/') ret = nil while url.size > 0 && ret.nil? ret = bundle_map[url.join('/')] url.pop end # Try root path if nothing found ret = bundle_map['/'] if ret.nil? # Return return (@current_bundle = ret) end # This method is called at the beginning of each request just in case # there as a build error last time around. def reset_current_bundle library.invalidate_bundle_caches @current_bundle = nil end # This method is used to redirect certain urls to an alternate bundle. If the # match phrase matches the url, then both the url we use to fetch resources and the # current_bundle will be swapped out. # # ===== Params # url<String>:: The url to check # match<Regex>:: The pattern to match and optionally later replace # new_bundle_name<Symbol>:: The name of the new bundle to swap in if matched # # ===== Returns # The rewritten url. May also change the value of current_bundle # def rewrite_bundle_if(url, match, new_bundle_name) return url unless match =~ url new_bundle = library.bundle_for(new_bundle_name) if new_bundle url = url.gsub(match, new_bundle.index_root) @current_bundle = new_bundle end return url end end end end