module MasterView # A DirectiveRegistry manages the directives available # for processing the MasterView directive attributes in # a template document. # # DirectiveRegistry is an internal mechanism of the # template engine, primarily used by the MasterView::Parser # to support directive attribute processing. # class DirectiveRegistry DEBUG_TRACE_LOADING = false #:nodoc: ##DEBUG## TRACE_MD_HARDENING_SUBJECT = nil #'MasterView::DirectiveTests::TestEventsDirective' DEBUG_BUILTIN_DIRECTIVE_REGISTRATION = false #:nodoc: DEBUG_CURRENT = false #:nodoc: # The DirectiveRegistry for managing the loaded directives available # for processing template markup. # # # DirectiveRegistry.current is ordinarily configured to load # the directives registered on the DirectiveLoadPath.current. def self.current @@current end # Set the current directives registry for template processing. # Ordinarily done once during masterview initialization. def self.current=(registry) #:nodoc: if self.class_variables.include?('@@current') && @@current && registry # HACK: need to preserve the list of loaded classes. # this shouldn't ordinarily happen, but repeated class loading # occurs during, ah, say... test suite runs, yes!! registry.update_loaded_classes_hack( @@current.loaded_classes ) end @@current = registry if DEBUG_CURRENT STDOUT.puts "\n****#{self.name}.current set: #{current.object_id}" STDOUT.puts "...#{current.inspect}\n" end end def update_loaded_classes_hack(already_loaded_classes) #:nodoc: @loaded_classes.concat( already_loaded_classes ) end # Register default namespaces for directive metadata # Should be invoked once during MasterView initialization def self.register_default_namespaces(ns_prefix_masterview, ns_prefix_extensions) #:nodoc: raise ArgumentError, "Invalid masterview namespace prefix '#{ns_prefix_masterview}'" if ns_prefix_masterview[-1..-1] != ':' raise ArgumentError, "Invalid extensions namespace prefix '#{ns_prefix_extensions}'" if ns_prefix_extensions[-1..-1] != ':' @@metadata_defaults_masterview = { :namespace => ns_prefix_masterview[0...-1], :namespace_prefix => ns_prefix_masterview, } @@metadata_defaults_extensions = { :namespace => ns_prefix_extensions[0...-1], :namespace_prefix => ns_prefix_extensions, } #assert NotReallyNecessary, 'because we'll just make sure we code up the right stuff here' DirectiveMetadata.validate_metadata_props! @@metadata_defaults_masterview DirectiveMetadata.validate_metadata_props! @@metadata_defaults_extensions end # Answer the namespace prefix of directives in the standard MasterView namespace. def mv_namespace_prefix ##ISSUE: needs work?? Does this need to be an inst var of the registry?? @@metadata_defaults_masterview[:namespace_prefix] end # Answer the namespace prefix of directives in the default MasterView # extensions directives namespace. def mv_extensions_namespace_prefix ##ISSUE: needs work?? Does this need to be an inst var of the registry?? @@metadata_defaults_extensions[:namespace_prefix] end # Answer the list of directive classes which are loaded in the current configuration. attr_reader :loaded_classes # Answer a list of all namespaces in use def loaded_namespaces @directive_namespaces #??.clone for paranoid safety? end # Answer the fully-qualified names of the registered directives def registered_directive_names @directive_classes.keys end # Answer the the registered directives def registered_directives @directive_classes.values end # Hash containing default directive metadata properties # Used during directive path loading to exploit default config specs. def metadata_defaults #:nodoc: @@metadata_defaults_masterview end # Hash containing default directive metadata properties # Used during directive path loading to exploit default config specs. def metadata_defaults_extensions #:nodoc: @@metadata_defaults_extensions end # internal mechanism to provide directory/path load context during directive class loading attr_reader :load_context #:nodoc: def set_load_context(context_settings, check_for_conflict=true) #:nodoc: if check_for_conflict && context_settings && @load_context raise RuntimeError, "Directive load operation already in progress: current context=#{@load_context.inspect}, requested context=#{context_settings.inspect}" end @load_context = context_settings end def initialize() @loaded_classes = [] set_load_context( nil ) clear_directive_maps() #STDOUT.puts "\n###Created DirectiveRegistry=#{self.object_id}" #STDOUT.puts "...loaded_classes=#{self.loaded_classes.inspect}" #STDOUT.puts "...load_context=#{self.load_context.inspect}" end protected def clear_directive_maps() #:nodoc: @directive_namespaces = [] @directive_classes = {} # map fully qualified directive attr name to directive_class @global_directives = [] # conditional global directives @auto_directives = [] # unconditional global directives end public #nasty modal access specification mechanism # Register a directive implementation. # # A directive is ordinarily a subclass of MasterView::DirectiveBase. # def register_directive(directive_class) # harden the DirectiveMetadata by filling in unspecified values from defaults #assert directive_class.ancestors.include? DirectiveMetadata ##AARGH: can't do this yet, the timing is premature [DJL 06-Oct-2006] #PUNT: md_defaults = load_context.nil? ? metadata_defaults_extensions : load_context[:metadata_defaults] #PUNT: directive_class.harden_metadata( md_defaults ) @loaded_classes << directive_class ##DEBUG## if TRACE_MD_HARDENING_SUBJECT and directive_class.name == TRACE_MD_HARDENING_SUBJECT STDOUT.puts "\nREGISTERED DIRECTIVE #{directive_class.name}" STDOUT.puts "...TOO SOON TO HARDEN METADATA" STDOUT.puts "...#{directive_class.metadata_values.object_id}: #{directive_class.metadata_values.inspect}\n" end ##DEBUG## end # Answer the (base) names of the loaded directive classes. # # By default, strips off module prefixes and returns just the # directive class name for brevity. # def loaded_class_names( simpleNames=true ) @loaded_classes.collect do |dc| simpleNames ? simple_class_name(dc) : dc.name end end # Answer the simple name of a directive class (without its module qualifier) def simple_class_name( dc ) dc.name.split(':').last end # Ensure that all directives on the load path are loaded. # Build the directive processing tables used by the template parser. # def process_directives_load_path( load_path=nil ) load_path = MasterView::DirectiveLoadPath.current if load_path.nil? load_directives( load_path ) build_directive_maps end # Ensure that all directives on the load path are loaded. # # require {directives_dir}/foo_directive.rb def load_directives( load_path=nil ) #:nodoc: load_path = MasterView::DirectiveLoadPath.current if load_path.nil? STDOUT.puts "\n-------- LOAD DIRECTIVES ON PATH -------" if DEBUG_TRACE_LOADING load_path.each do | dpe | STDOUT.puts "DIRECTORY PATH ENTRY: #{dpe.inspect}" if DEBUG_TRACE_LOADING if dpe.exists? self.load_from_directive_path_entry( dpe ) else #raise InvalidPathError.new('Directive load path dir does not exist:'+dir_path) Log.error "Directive load path dir does not exist: '#{dpe.dir_path}'" if MasterView.const_defined?(:Log) #backstop for test case startup end end clear_directive_maps() # ensure we take a clean point of view on whatever just got loaded STDOUT.puts "------ END LOAD DIRECTIVES ------" if DEBUG_TRACE_LOADING end # Load a specific directive implementation class # # Mainly provided for use by unit tests def load_directive_file(directive_file_path) dir_path = File.dirname(directive_file_path) dpe = MasterView::DirectiveLoadPath::PathEntry.new(dir_path) load_from_directive_path_entry( dpe, directive_file_path ) end def load_from_directive_path_entry(dpe, directive_file_path=nil) #:nodoc: dir_md_specs = dpe.load_metadata_specs # :create_if_not_defined => true is_mv_directives_dir = dir_md_specs.fetch(:use_masterview_namespace, false) app_md_defaults = is_mv_directives_dir ? metadata_defaults : metadata_defaults_extensions md_defaults = MasterView::DirectiveLoadPath.compute_md_defaults( app_md_defaults, dir_md_specs[:default], dpe.metadata_defaults ) begin dir_load_context = { :metadata_defaults => md_defaults, } set_load_context( dir_load_context ) if directive_file_path # load specific file from a directive path entry directory dir_load_context[:directive_file] = directive_file_path load_single_directive_file(directive_file_path, md_defaults) #WAS: require directive_file_path else # load all directives in the directory dir_path = dpe.dir_path #??Log.debug { "Loading directives from #{dir_path} with metadata defaults #{md_defaults.inspect}" } #??load_context[:synonyms] = dir_md_specs.fetch(:synonyms, {})?? files_to_ignore = dir_md_specs.fetch(:ignore, []).collect { |fn| fn.ends_with?('.rb') ? fn : "#{fn}.rb" } Dir.entries( dir_path ).each { |fn| # we assume all .rb files in a directives dir are directive impls if fn =~ /[.]rb$/ next if files_to_ignore.include?(fn) # skip directives in the ignore list directive_file_path = "#{dir_path}/#{fn}" dir_load_context[:directive_file] = directive_file_path #??@load_context[:directive_synonyms] = dir_synonyms_specs.fetch(fn, [])?? load_single_directive_file(directive_file_path, md_defaults) #WAS: require directive_file_path end } end ensure set_load_context( nil ) end end protected # we can't to this in register_directive as desired because # the class defn hasn't been processed yet at that point, it seems. def load_single_directive_file(directive_file_path, md_defaults) last_class_loaded = @loaded_classes.last require directive_file_path if @loaded_classes.last != last_class_loaded directive_class = @loaded_classes.last directive_class.harden_metadata( md_defaults ) ##DEBUG## if TRACE_MD_HARDENING_SUBJECT and directive_class.name == TRACE_MD_HARDENING_SUBJECT STDOUT.puts "\nFINISHED LOADING DIRECTIVE #{directive_class.name}" STDOUT.puts "...SHOULD HAVE HARDENED METADATA NOW:" STDOUT.puts "...#{directive_class.metadata_values.object_id}: #{directive_class.metadata_values.inspect}\n" STDOUT.puts "" end ##DEBUG## end end public #nasty modal access specification mechanism # Build the directive processing tables used by the template parser. def build_directive_maps() #:nodoc: clear_directive_maps() # ensure we take a clean point of view on the matter at hand Log.debug { 'directive plugins loaded:' + loaded_class_names.inspect } if MasterView.const_defined?(:Log) #backstop for test case startup loaded_classes.each do |dc| dc.on_load if dc.respond_to?(:on_load) #ISSUE: needs review (timing, intent) [DJL 08-Oct-2006] attr_qname = dc.attribute_qname ns_name = dc.namespace_name raise NameError, "Directive qname requires namespace: '#{attr_qname} - #{dc.name}" if ns_name.nil? || ns_name.strip().empty? if DEBUG_BUILTIN_DIRECTIVE_REGISTRATION and dc.name.starts_with?('MasterView::Directives::') if ns_name != 'mv' #! attr_qname.starts_with(MasterView.mv_namespace_prefix) STDOUT.puts "****BUILTIN DIRECTIVE NAMESPACE PROBLEM: #{dc.name} qname='#{attr_qname}'" raise RuntimeError, "BUILTIN DIRECTIVE NS PROBLEM: #{dc.name} qname='#{attr_qname}" end end @directive_namespaces << ns_name if ! @directive_namespaces.include?( ns_name ) @directive_classes[attr_qname] = dc # global directives can be automatically attached to all elements (e.g., generated comments) # or conditionally attached (e.g., a directive that wants to glom onto all elements of a specific type) dc_global_spec = dc.metadata_values.fetch(:global_directive?, false) if dc_global_spec == true # unconditional global directive - applied to all elements @auto_directives << dc elsif dc_global_spec # conditional global directive - applied when match condition satisfied @global_directives << dc end end if MasterView.const_defined?(:Log) #backstop for test case startup Log.debug { "directives=#{@directive_classes.keys().sort!().inspect}" } Log.debug { "global_directives=#{@global_directives.inspect}" } Log.debug { "auto_directives=#{@auto_directives.inspect}" } end end # Construct directive processors needed to handle the # attributes defined on a template document element. # # Constructs processors for all global directives # and for any directive attributes defined on the element. # # Removes all directive attributes from the element's attributes. # def construct_directive_processors( tag_name, element_attrs ) directive_processors = [] # always instantiate global directive handlers @auto_directives.each do | dc | directive_processors << dc.new(nil) end # conditionally instantiate global directives which are interested in this element @global_directives.each do | dc | attr_value = dc.metadata_values[:global_directive?].call( tag_name, element_attrs ) if attr_value directive_processors << dc.new(attr_value) end end # instantiate the directive processor on the attribute value if its attr present # remove the MV attribute from the document so that its only effect is from the processor action @directive_classes.each do | attr_qname, dc | directive_attr_value = element_attrs[attr_qname] if directive_attr_value # convert the directive into a processing handler for this element element_attrs.delete(attr_qname) directive_processors << dc.new(directive_attr_value) end end directive_processors end # class initialization self.current = self.new() #STDOUT.puts "INITIALIZED DirectiveRegistry.current=#{DirectiveRegistry.current.object_id}" ##DEBUG## #STDOUT.puts "...DirectiveRegistry.current=#{DirectiveRegistry.current.inspect}" ##DEBUG## #STDOUT.puts "END DirectiveRegistry definition\n" ##DEBUG## end # Register a directive implementation. # # Registration is handled automatically for directives # implemented as subclasses of MasterView::DirectiveBase, # the usual technique, or by including the PluginLoadTracking # module in a directive implementation class. # # Directive registration ordinarily occurs during MasterView # initialization, when directive classes from the configured # directive_load_path directories are automatically # loaded and registered with the template engine. # def self.register_directive(directive_class) DirectiveRegistry.current.register_directive(directive_class) end end