lib/action_view/template/resolver.rb in actionpack-3.2.22.5 vs lib/action_view/template/resolver.rb in actionpack-4.0.0.beta1

- old
+ new

@@ -1,16 +1,17 @@ require "pathname" require "active_support/core_ext/class" -require "active_support/core_ext/io" -require "active_support/core_ext/string/starts_ends_with" +require "active_support/core_ext/class/attribute_accessors" require "action_view/template" +require "thread" +require "thread_safe" module ActionView # = Action View Resolver class Resolver # Keeps all information about view path and builds virtual path. - class Path < String + class Path attr_reader :name, :prefix, :partial, :virtual alias_method :partial?, :partial def self.build(name, prefix, partial) virtual = "" @@ -18,83 +19,135 @@ virtual << (partial ? "_#{name}" : name) new name, prefix, partial, virtual end def initialize(name, prefix, partial, virtual) - @name, @prefix, @partial = name, prefix, partial - super(virtual) + @name = name + @prefix = prefix + @partial = partial + @virtual = virtual end + + def to_str + @virtual + end + alias :to_s :to_str end + # Threadsafe template cache + class Cache #:nodoc: + class SmallCache < ThreadSafe::Cache + def initialize(options = {}) + super(options.merge(:initial_capacity => 2)) + end + end + + # preallocate all the default blocks for performance/memory consumption reasons + PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new} + PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)} + NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)} + KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)} + + # usually a majority of template look ups return nothing, use this canonical preallocated array to safe memory + NO_TEMPLATES = [].freeze + + def initialize + @data = SmallCache.new(&KEY_BLOCK) + end + + # Cache the templates returned by the block + def cache(key, name, prefix, partial, locals) + if Resolver.caching? + @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield) + else + fresh_templates = yield + cached_templates = @data[key][name][prefix][partial][locals] + + if templates_have_changed?(cached_templates, fresh_templates) + @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates) + else + cached_templates || NO_TEMPLATES + end + end + end + + def clear + @data.clear + end + + private + + def canonical_no_templates(templates) + templates.empty? ? NO_TEMPLATES : templates + end + + def templates_have_changed?(cached_templates, fresh_templates) + # if either the old or new template list is empty, we don't need to (and can't) + # compare modification times, and instead just check whether the lists are different + if cached_templates.blank? || fresh_templates.blank? + return fresh_templates.blank? != cached_templates.blank? + end + + cached_templates_max_updated_at = cached_templates.map(&:updated_at).max + + # if a template has changed, it will be now be newer than all the cached templates + fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } + end + end + cattr_accessor :caching self.caching = true class << self alias :caching? :caching end def initialize - @cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| - h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } + @cache = Cache.new end def clear_cache - @cached.clear + @cache.clear end # Normalizes the arguments and passes it on to find_template. def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) cached(key, [name, prefix, partial], details, locals) do - find_templates(name, prefix, partial, details, false) + find_templates(name, prefix, partial, details) end end - def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[]) - cached(key, [name, prefix, partial], details, locals) do - find_templates(name, prefix, partial, details, true) - end - end - private - delegate :caching?, :to => "self.class" + delegate :caching?, to: :class # This is what child classes implement. No defaults are needed # because Resolver guarantees that the arguments are present and # normalized. - def find_templates(name, prefix, partial, details, outside_app_allowed = false) - raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed) method" + def find_templates(name, prefix, partial, details) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" end # Helpers that builds a path. Useful for building virtual paths. def build_path(name, prefix, partial) Path.build(name, prefix, partial) end # Handles templates caching. If a key is given and caching is on # always check the cache before hitting the resolver. Otherwise, - # it always hits the resolver but check if the resolver is fresher - # before returning it. + # it always hits the resolver but if the key is present, check if the + # resolver is fresher before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info locals = locals.map { |x| x.to_s }.sort! - if key && caching? - @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) - else - fresh = decorate(yield, path_info, details, locals) - return fresh unless key - - scope = @cached[key][name][prefix][partial] - cache = scope[locals] - mtime = cache && cache.map(&:updated_at).max - - if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime } - scope[locals] = fresh - else - cache + if key + @cache.cache(key, name, prefix, partial, locals) do + decorate(yield, path_info, details, locals) end + else + decorate(yield, path_info, details, locals) end end # Ensures all the resolver information is set in the template. def decorate(templates, path_info, details, locals) #:nodoc: @@ -115,25 +168,27 @@ def initialize(pattern=nil) @pattern = pattern || DEFAULT_PATTERN super() end - cattr_accessor :instance_reader => false, :instance_writer => false - private - def find_templates(name, prefix, partial, details, outside_app_allowed = false) + def find_templates(name, prefix, partial, details) path = Path.build(name, prefix, partial) - query(path, details, details[:formats], outside_app_allowed) + query(path, details, details[:formats]) end - def query(path, details, formats, outside_app_allowed) + def query(path, details, formats) query = build_query(path, details) - template_paths = find_template_paths query + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed + template_paths = Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } template_paths.map { |template| handler, format = extract_handler_and_format(template, formats) contents = File.binread template @@ -142,40 +197,10 @@ :format => format, :updated_at => mtime(template)) } end - def reject_files_external_to_app(files) - files.reject { |filename| !inside_path?(@path, filename) } - end - - if RUBY_VERSION >= '2.2.0' - def find_template_paths(query) - Dir[query].reject { |filename| - File.directory?(filename) || - # deals with case-insensitive file systems. - !File.fnmatch(query, filename, File::FNM_EXTGLOB) - } - end - else - def find_template_paths(query) - # deals with case-insensitive file systems. - sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - - Dir[query].reject { |filename| - File.directory?(filename) || - !sanitizer[File.dirname(filename)].include?(filename) - } - end - end - - def inside_path?(path, filename) - filename = File.expand_path(filename) - path = File.join(path, '') - filename.start_with?(path) - end - # Helper for building query glob string based on resolver's pattern. def build_query(path, details) query = @pattern.dup prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" @@ -204,17 +229,25 @@ # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format(path, default_formats) pieces = File.basename(path).split(".") pieces.shift - handler = Template.handler_for_extension(pieces.pop) - format = pieces.last && Mime[pieces.last] + + extension = pieces.pop + unless extension + message = "The file #{path} did not specify a template handler. The default is currently ERB, " \ + "but will change to RAW in the future." + ActiveSupport::Deprecation.warn message + end + + handler = Template.handler_for_extension(extension) + format = pieces.last && Template::Types[pieces.last] [handler, format] end end - # A resolver that loads files from the filesystem. It allows to set your own + # A resolver that loads files from the filesystem. It allows setting your own # resolving pattern. Such pattern can be a glob string supported by some variables. # # ==== Examples # # Default pattern, loads views the same way as previous versions of rails, eg. when you're @@ -226,11 +259,11 @@ # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. # # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}") # - # If you don't specify pattern then the default will be used. + # If you don't specify a pattern then the default will be used. # # In order to use any of the customized resolvers above in a Rails application, you just need # to configure ActionController::Base.view_paths in an initializer, for example: # # ActionController::Base.view_paths = FileSystemResolver.new( @@ -238,14 +271,14 @@ # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" # ) # # ==== Pattern format and variables # - # Pattern have to be a valid glob string, and it allows you to use the + # Pattern has to be a valid glob string, and it allows you to use the # following variables: # - # * <tt>:prefix</tt> - usualy the controller path + # * <tt>:prefix</tt> - usually the controller path # * <tt>:action</tt> - name of the action # * <tt>:locale</tt> - possible locale versions # * <tt>:formats</tt> - possible request formats (for example html, json, xml...) # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) # @@ -269,15 +302,10 @@ # An Optimized resolver for Rails' most common case. class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: def build_query(path, details) exts = EXTENSIONS.map { |ext| details[ext] } - - if path.to_s.starts_with? @path.to_s - query = escape_entry(path) - else - query = escape_entry(File.join(@path, path)) - end + query = escape_entry(File.join(@path, path)) query + exts.map { |ext| "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" }.join end