Class: Bijou::Processor

Inherits:
Object
  • Object
show all
Defined in:
lib/bijou/processor.rb

Overview

The processor encapsulates the loading and parsing of a component class and any referenced containers or components.

Constant Summary collapse

ComponentPrefix =
'Component_'
CacheSuffix =
'_cache'
CacheSubdir =
'cache'
PathSepRE =
/\/|\\/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeProcessor

Returns a new instance of Processor.



27
28
29
# File 'lib/bijou/processor.rb', line 27

def initialize()
  @trace = false
end

Class Method Details

.class_from_filename(prefix, filename) ⇒ Object



361
362
363
# File 'lib/bijou/processor.rb', line 361

def self.class_from_filename(prefix, filename)
  return prefix + Digest::SHA1.hexdigest(filename.downcase).downcase
end

.create_component(context, class_text, class_name) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/bijou/processor.rb', line 273

def self.create_component(context, class_text, class_name)
  #
  # If the class is already in memory, we'll need to undefine it. 
  # Otherwise, the new definition would be appended to the old one.
  #
  module_name = "Object" # REVIEW: This is the default right now.
  if Object.const_defined? module_name
    module_ref = Object.const_get(module_name)

    if module_ref.const_defined? class_name
      module_ref.remove_const(class_name)
    end
  end

  # REVIEW: If we can scope eval, we can use a random name.
  eval(class_text)
  component_class = eval(class_name)

  component_object = component_class.new(context)
  context.add_component(component_object)

  return component_object
end

.ensure_directory_exists_for_file(filename) ⇒ Object



389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/bijou/processor.rb', line 389

def self.ensure_directory_exists_for_file filename
  if filename !~ PathSepRE
    raise "Invalid filename"
  end

  n = filename.rindex(PathSepRE)
  path = filename[0, n]

  if !File.directory?(path)
    # NOTE: mkdir_p works with or without a / terminator.
    FileUtils.mkdir_p(path)
  end
end

.execute(context, class_text, class_name, args = {}) ⇒ Object

A convenience function that parses and renders a class. It will not load or cache component documents, like load_component, nor will containers or components be automatically handled. The caller must set callbacks to properly handle these load events.



301
302
303
304
305
306
307
# File 'lib/bijou/processor.rb', line 301

def self.execute(context, class_text, class_name, args={})
  component_object = self.create_component(context, class_text, class_name)

  context.render(args)

  return context.output
end

.handle_other(config, path_info) ⇒ Object

This helper method can be used with certain web server adapters to simplify the process of serving normal content types. Many web servers will delegate specific requests to the Bijou adapter, based on the request. Some servers do not delegate based on file name patterns and thus the adapter must disambiguate and serve such requests.



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/bijou/processor.rb', line 314

def self.handle_other(config, path_info)
  content_types = 
    [
     [ 'txt',  'text/plain' ],
     [ 'htm',  'text/html'  ],
     [ 'html', 'text/html'  ],
     [ 'css',  'text/css'   ],
     [ 'gif',  'image/gif'  ],
     [ 'jpg',  'image/jpeg' ],
     [ 'jpeg', 'image/jpeg' ],
     [ 'png',  'image/png'  ],
     [ 'tif',  'image/tiff' ],
     [ 'tiff', 'image/tiff' ],
     [ 'bmp',  'image/x-ms-bmp' ],
    ]

  path = File.expand_path(path_info, config.document_root)

  content_type = 'text/plain'
  content_types.each {|type|
    ext = type[0]

    if path_info =~ /\.#{ext}$/
      content_type = type[1]
      break
    end
  }

  if File.exists?(path)
    file = File.new(path, "r")
    response = {
      'status' => 200,
      'type' => content_type,
      'body' => file.read
    }
    file.close
  else
    response = {
      'status' => 404,
      'type' => content_type,
      'body' => ''
    }
  end

  return response
end

.is_cache_stale(filename, cachename) ⇒ Object



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/bijou/processor.rb', line 365

def self.is_cache_stale(filename, cachename)
  parsefile = true

  if !File.exists?(cachename)
    return true # The file hasn't been cached yet.
  end

  cachemod = File.stat(cachename).mtime

  if !File.exists?(filename)
    raise "Source file not found"
  end

  sourcemod = File.stat(filename).mtime

  if sourcemod <= cachemod
    # The file has been cached and the source hasn't been touched.
    return false
  end

  # The cached file is older than the source.
  return true
end

Instance Method Details

#cache_check(config, filename, cachename) ⇒ Object

Returns true if the file is cached and the cache is fresh.



32
33
34
35
36
37
38
39
40
# File 'lib/bijou/processor.rb', line 32

def cache_check(config, filename, cachename)
  if !config.cache
    # Don't cache anything
    trace "don't cache"
    return false
  end
  
  return !Processor.is_cache_stale(filename, cachename)
end

#create_class_object(class_name, context) ⇒ Object



84
85
86
87
# File 'lib/bijou/processor.rb', line 84

def create_class_object(class_name, context)
  component_class = eval(class_name)
  return component_class.new(context)
end

#get_cache_path(config, path) ⇒ Object

Returns true if the file is cached and the cache is fresh.



73
74
75
76
77
78
79
80
81
82
# File 'lib/bijou/processor.rb', line 73

def get_cache_path(config, path)
  cache_root = get_cache_root(config)

  cache_path = File.expand_path(path, cache_root)
  if config.cache_ext
    cache_path << config.cache_ext
  end

  return cache_path
end

#get_cache_root(config) ⇒ Object

Returns a cache directory based on the configuration.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/bijou/processor.rb', line 47

def get_cache_root(config)
  if config.cache_root
    # Store in the cache subdirectory of the cache root. We do this is
    # in case we need to store additional files.
    cache_root = File.expand_path('cache', config.cache_root)
  elsif config.cache_ext
    # Use the specified extension and store in the document root.
    cache_root = config.document_root
  else
    # Neither specified; create a cache root parallel to the doc root.

    # Strip the trailing slash.
    cache_root = File.expand_path('', config.document_root)

    # Give it a different path name, parallel to the document root.
    cache_root << CacheSuffix

    # Store in the cache subdirectory of the cache root. We do this is
    # in case we need to store additional files.
    cache_root = File.expand_path('cache', cache_root)
  end

  return cache_root
end

#load(path, cfg = nil) ⇒ Object

This is the workhorse of the processor. It utilizes several classes, including the parser, to load and parse a component in preparation for rendering. The return value is a Context object that may be used to render the component one or more times, each with a different set of arguments.

The cfg argument may be either a Bijou::Config or a Bijou::Context.



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
# File 'lib/bijou/processor.rb', line 122

def load(path, cfg=nil)
  context = nil
  config = nil

  # Initialize the context and config.
  if cfg
    if cfg.kind_of?(Bijou::Config)
      config = cfg
    elsif cfg.kind_of?(Bijou::Context)
      context = cfg
      config = context.config
    else
      raise "If specified, cfg must be a Context or Config object"
    end
  else
    # Provide a default.
    config = Bijou::Config.new
  end

  if !context
    # Create the top-level context.
    context = Bijou::Context.new(config)
  end

  if config.document_root
    if !File.directory?(config.document_root)
      raise "The documet_root is not a valid directory."
    end
  else
    # NOTE: This modifies the configuration.
    config.document_root = Dir.getwd
  end

  # We want the processor to handle container and component creation.
  context.container_callback = method(:load_container_callback)
  context.component_callback = method(:load_component_callback)

  # Load the top-level component as a normal component.
  load_component(context, path)
end

#load_component(context, path) ⇒ Object



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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/bijou/processor.rb', line 163

def load_component(context, path)
  config = context.config

  if !config.document_root
    raise "A document_root must be specified"
  end

  path = path.strip

  # TODO: Consolidate tracing to a single path.
  trace "load_component #{path}"
  context.trace(Bijou::Log::Info, "load_component(#{path})")

  # The caller should strip the slash, but we do it here as a safeguard.
  if path[0,1] =~ PathSepRE
    path = path[1..-1]
  end

  filename = File.expand_path(path, config.document_root)
  trace "filename #{filename}"

  if !File.exists?(filename)
    raise Bijou::FileNotFound.new(filename), "File not found: #{filename}"
  end

  classname = Processor.class_from_filename(ComponentPrefix, filename)
  trace "classname #{classname}"

  # context.trace(Bijou::Log::Info, "get_cache_path #{config.cache_root || ''}");
  cachename = get_cache_path(config, path)
  trace "cachename #{cachename}"

  component_class = nil

  if !cache_check(config, filename, cachename)
    context.trace(Bijou::Log::Info, "parse and load into cache #{cachename}");
    Processor.ensure_directory_exists_for_file cachename

    parser = Bijou::Parser.new

    trace "parse #{classname}"
    source_file = File.new(filename)
    parser.parse_file(classname, source_file, path)
    source_file.close

    class_text = parser.render(classname, filename, cachename, 
                               config.component_base, config.require_list)
    
    # Write the new class text to the cache file.
    if parser.diagnostics.errors.length == 0 && class_text
      object_file = File.open(cachename, "w")
      object_file.write(class_text)
      object_file.close
    else
      File.delete(cachename) if File.exist?(cachename)
      
      raise Bijou::ParseError.new(filename, parser.diagnostics), 
      "Parse error for file '#{path}'"
    end
  else
    begin
      #
      # If the class is already in memory, avoid hitting the disk.
      #
      trace "try read from memory #{classname}"
      component_object = create_class_object(classname, context)

      context.add_component(component_object)
    rescue NameError
      trace "not in memory"
    end

    if component_object
      context.trace(Bijou::Log::Info, "  create from memory #{cachename}");
    else
      context.trace(Bijou::Log::Info, "  create from cache #{cachename}");

      #
      # Otherewise, read the class text from the cache.
      #
      trace "read from cache #{cachename}"

      object_file = File.open(cachename, "r")
      class_text = object_file.read
      object_file.close
    end
  end

  if !component_object
    #
    # Create component using class text, while putting the definition 
    # into memory.
    #
    trace "create_component"
    begin
      component_object = Processor.create_component(context, 
                                                    class_text, 
                                                    classname)
    rescue SyntaxError
      raise Bijou::EvalError.new(filename, cachename, $!.message),
      "Syntax error in file '#{path}'"
    end
  end

  trace "load finished #{path}"

  # Caller will need to call context.render(args)
  return context
end

#load_component_callback(context, path, args) ⇒ Object

When a component is requested, either directly, by context.invoke, or indirectly, using the <& … &> syntax, the context delegates the loading of the component to the owner of the context (if the component_callback was registered).



103
104
105
106
107
108
109
110
111
112
# File 'lib/bijou/processor.rb', line 103

def load_component_callback(context, path, args)
  trace "component_callback #{path}"

  subcontext = context.clone

  load_component(subcontext, path)

  subcontext.render(args)
  return subcontext.output
end

#load_container_callback(context, path) ⇒ Object

Handles loading of containers. The context invokes this callback when a component is loaded and the component has an associated container. The context class is environment agnostic and thus doesn’t know how to load a container. Instead, it delegates the mechanics to the context owner.



93
94
95
96
97
# File 'lib/bijou/processor.rb', line 93

def load_container_callback(context, path)
  trace "container_callback #{path}"

  load_component(context, path)
end

#trace(msg) ⇒ Object



42
43
44
# File 'lib/bijou/processor.rb', line 42

def trace(msg)
  puts msg if @trace
end