aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view/digestor.rb
blob: d31154061cc5ccecd6c56bc0eb158b7117087dd2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
require 'concurrent/map'
require 'action_view/dependency_tracker'
require 'monitor'

module ActionView
  class Digestor
    cattr_reader(:cache)
    @@cache          = Concurrent::Map.new
    @@digest_monitor = Monitor.new

    class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
      def call(env)
        ActionView::Digestor.cache.clear
        app.call(env)
      end
    end

    class << self
      # Supported options:
      #
      # * <tt>name</tt>   - Template name
      # * <tt>finder</tt>  - An instance of <tt>ActionView::LookupContext</tt>
      # * <tt>dependencies</tt>  - An array of dependent views
      # * <tt>partial</tt>  - Specifies whether the template is a partial
      def digest(name:, finder:, **options)
        options.assert_valid_keys(:dependencies, :partial)

        cache_key = ([ name, finder.details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')

        # this is a correctly done double-checked locking idiom
        # (Concurrent::Map's lookups have volatile semantics)
        @@cache[cache_key] || @@digest_monitor.synchronize do
          @@cache.fetch(cache_key) do # re-check under lock
            compute_and_store_digest(cache_key, name, finder, options)
          end
        end
      end

      private
        def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock
          klass = if options[:partial] || name.include?("/_")
            # Prevent re-entry or else recursive templates will blow the stack.
            # There is no need to worry about other threads seeing the +false+ value,
            # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
            pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
            PartialDigestor
          else
            Digestor
          end

          @@cache[cache_key] = stored_digest = klass.new(name, finder, options).digest
        ensure
          # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
          @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
        end
    end

    EMPTY = Class.new {
      def name; 'missing'; end
      def digest; ''; end
    }.new

    def self.tree(name, finder, partial = false, seen = {})
      if obj = seen[name]
        obj
      else
        logical_name = name.gsub(%r|/_|, "/")
        template = finder.disable_cache { finder.find(logical_name, [], partial) }
        node = seen[name] = Node.new(name, logical_name, template, partial, [])
        deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
        deps.each do |dep_file|
          l_name = dep_file.gsub(%r|/_|, "/")
          if finder.disable_cache { finder.exists?(l_name, [], true) }
            node.children << tree(dep_file, finder, true, seen)
          else
            node.children << Missing.new(dep_file, l_name, nil, true, [])
          end
        end
        node
      end
    end

    class Node < Struct.new(:name, :logical_name, :template, :partial, :children)
      def to_dep(finder)
        if partial
          PartialDigestor.new(name, finder, partial: partial)
        else
          Digestor.new(name, finder, partial: partial)
        end
      end

      def digest
        Digest::MD5.hexdigest("#{template.source}-#{dependency_digest}")
      end

      def dependency_digest
        children.map(&:digest).join("-")
      end
    end

    class Missing < Node
      def digest
        ''
      end
    end

    attr_reader :name, :finder, :options

    def initialize(name, finder, options = {})
      @name, @finder = name, finder
      @options = options
    end

    def digest
      Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
        logger.debug "  Cache digest for #{template.inspect}: #{digest}"
      end
    rescue ActionView::MissingTemplate
      logger.error "  Couldn't find template for digesting: #{name}"
      ''
    end

    def dependencies
      DependencyTracker.find_dependencies(name, template, finder.view_paths)
    rescue ActionView::MissingTemplate
      logger.error "  '#{name}' file doesn't exist, so no dependencies"
      []
    end

    def children
      dependencies.collect do |dependency|
        PartialDigestor.new(dependency, finder)
      end
    end

    def nested_dependencies
      dependencies.collect do |dependency|
        dependencies = PartialDigestor.new(dependency, finder).nested_dependencies
        dependencies.any? ? { dependency => dependencies } : dependency
      end
    end

    private
      class NullLogger
        def self.debug(_); end
        def self.error(_); end
      end

      def logger
        ActionView::Base.logger || NullLogger
      end

      def logical_name
        name.gsub(%r|/_|, "/")
      end

      def partial?
        false
      end

      def template
        @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
      end

      def source
        template.source
      end

      def dependency_digest
        template_digests = dependencies.collect do |template_name|
          Digestor.digest(name: template_name, finder: finder, partial: true)
        end

        (template_digests + injected_dependencies).join("-")
      end

      def injected_dependencies
        Array.wrap(options[:dependencies])
      end
  end

  class PartialDigestor < Digestor # :nodoc:
    def partial?
      true
    end
  end
end