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