# # File:: treecore.rb # Description:: Content system core content tree node class implementation. # Author:: David Muir # Author:: Greg Smith # Date:: 12 June 2008 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/log/log' require 'rexml/document' include REXML #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline module Content # # == Description # The Base content node class. # # The node's dirty flag can be used by tools to flag certain content nodes # as being dirty (e.g. Rage convert can use it to ensure that dirty content # is reconverted). # # To read the dirty flag use the dirty? method. # class Base attr_reader :name # String name of content attr_reader :inputs # Array of Content objects (dependencies) attr_reader :outputs # Array of Content objects (outputs) attr_reader :parent # Parent reference attr_reader :xml_type # XML content type string XML_CONTENT_TYPE = 'base' def to_s( ) "CONTENT id:#{self.object_id} name:#{@name} type:#{@xml_type} parent:#{@parent.object_id} inputs(#{@inputs.size}) outputs(#{@outputs.size})" end def ===( other ) ( ( @name === other.name ) and ( @xml_type == other.xml_type ) ) end # # Return dirty flag, virtual attribute. # def dirty?() @dirty end # # Set dirty flag, default to true and not to recurse to input nodes. # def set_dirty( dirty = true, recurse = false ) @dirty = dirty @inputs.each do |input| input.set_dirty( dirty, recurse ) end end # # Returns content node outdated status. # def outdated?( env ) return true if dirty? @inputs.each do |input| return true if input.outdated?( env ) end false end def pretty_print( display_inputs = false, indent = 0, indent_char = "\t" ) indent.times do print( indent_char ); end print( "#{self}\n" ) # Print input nodes... if ( display_inputs ) then @inputs.each do |n| (indent+1).times do print( indent_char ); end print( "INPUT #{n}\n" ) end end end def fill_env( env ) env.add( 'name', @name ) unless ( nil == @name ) env.add( 'type', @xml_type ) end # # Walk a content hierarchy invoking block argument for each content # node. # def walk( &block ) yield self if block_given? @inputs.each do |input| input.walk( &block ) end end # # Add a Content node to our array of inputs. # def add_input( input ) throw ArgumentError.new( 'Cannot add nil input.' ) \ if ( input.nil? ) throw TypeError.new( "Cannot add non-Content input (class: #{input.class})." ) \ unless ( input.is_a?( Base ) ) @inputs << input end # Add a Content node to our array of outputs. def add_output( output ) throw ArgumentError.new( 'Cannot add nil output.' ) \ if ( output.nil? ) throw TypeError.new( "Cannot add non-Content output (class: #{output.class})." ) \ unless ( output.is_a?( Base ) ) @outputs << output end # Virtual method which is invoked after the tree is loaded to resolve # input nodes. def post_load_input( ) end # Virtual method which is invoked after the tree is loaded to resolve # output nodes. def post_load_output( ) end #--------------------------------------------------------------------- # Protected Methods #--------------------------------------------------------------------- protected def initialize( name ) @@log = Log.new( 'contenttree' ) if ( nil == @@log ) @name = ( nil == name ) ? '' : name @inputs = [] @outputs = [] @xml_type = XML_CONTENT_TYPE @dirty = false end # Protected write accessor for 'parent' attribute. def parent=( p ) @parent = p end #--------------------------------------------------------------------- # Dynamic Content Classes Support # This is the magic that allows the Parser to know about all of our # defined content types. #--------------------------------------------------------------------- def self.inherited( child ) # DHM TODO:: do some sanity tests on our content classes before # registering them into our system. Checks some simple # requirements, e.g. from_xml class method, type string unique etc. Base.registered_content_types << child end @registered_content_types = [] class << self; attr_reader :registered_content_types end #--------------------------------------------------------------------- # Protected Variables #--------------------------------------------------------------------- protected @@log = nil end # # == Description # Unresolved objects are created by the parser for inputs that cannot be # resolved when parsing. As with Unknown objects, these likely indicate a # misconfiguration in the content XML. # class Unresolved < Base attr_reader :input_name attr_reader :input_type attr_reader :input_group XML_CONTENT_TYPE = 'unresolved' def initialize( input_name, input_type, input_group ) super( 'unresolved' ) @xml_type = 'UNRESOLVED' @input_name = input_name @input_type = input_type @input_group = input_group end def to_s( ) "#{super()} input_name:\"#{input_name}\" input_type:\"#{input_type}\" input_group:\"#{input_group}\"" end end # # == Description # The SupportChildren module defines attributes and functions for supporting # an array of child content nodes. This module can then be mixed-in with # content nodes which require child support that are in a different # class hierarchy path (e.g. Group, RPF). # # This module contains children declaration, walk method and a collection # of search methods. # # NOTE: If you define a class which includes this module you must call the # init_SupportChildren method or you will get NilClass errors if you access # members of the children member. # module SupportChildren #don't return the array directly - return a frozen copy #optimised find_first method relies on the fact that #the children array is not modified outside of this class #attr_reader :children # Array of child Content objects def children return Array.new(@children).freeze end def children=( new_children ) @children = new_children end def init_SupportChildren( ) @children = [] end # # Set dirty flag, default to true and not to recurse to input nodes. # def set_dirty( dirty = true, recurse = false ) super( dirty, recurse ) @children.each do |child| child.set_dirty( dirty, recurse ) end end # # A node with children is out of date if any of its children are # outdated # def outdated?( env ) @children.each do |child| return true if child.outdated?( env ) end false end def pretty_print( display_inputs = false, indent = 0, indent_char = "\t" ) indent.times do print( indent_char ); end print( "#{self}\n" ) if ( display_inputs ) then @inputs.each do |n| (indent+1).times do print( indent_char ); end print( "INPUT #{n}\n" ) end end if ( @children.size > 0 ) @children.each do |c| c.pretty_print( display_inputs, (indent+1), indent_char ) end else (indent+1).times do print( indent_char ); end print( "NO CHILDREN\n" ) end end # # Walk a content hierarchy invoking block argument for each content # node. # def walk( &block ) yield self if block_given? @children.each do |c| c.walk( &block ) end end # # Add a child content node into this content group. # def add_child( child_content ) throw ArgumentError.new( 'Cannot add nil children.' ) \ if ( child_content.nil? ) throw TypeError.new( "Cannot add non-Content children (class: #{child_content.class})." ) \ unless ( child_content.is_a?( Base ) ) # throw RuntimeError::new( "Cannot add child who already has a parent (parent: #{child_content.parent})." ) \ # unless ( child_content.parent.nil? ) @children << child_content child_content.parent = self end # # Remove a child content node from this content group. # def remove_child( child_content ) @children.delete( child_content ) end # # Return an Array of all the found content nodes where the passed block # evaluates to true. # # == Example Usage # project.content.find( ) do |content| # content.is_a?( Content::File ) # end # def find( &block ) return ( [] ) unless ( block_given? ) found = [] @children.each do |content| found << content if ( yield content ) # Recurse... if ( content.methods.include?( 'find' ) ) found += content.find( &block ) end end found end # # Similar to 'find' except the objects in the Array are the items # returned by the block (if non-nil). # # == Example Usage # project.content.collect( ) do |content| # if ( content.is_a?( Content::File ) ) # content.filename # else # nil # end # end # def collect( &block ) return ( [] ) unless ( block_given? ) found = [] @children.each do |content| result = ( yield content ) found << result unless ( result.nil? ) # Recurse... if ( content.methods.include?( 'collect' ) ) found += content.collect( &block ) end end found end # # Find first content node with the specified name and type. Returns # a subclass of Pipeline::Content::Base or nil. # def find_first( findname, findtype = '' ) safefindtype = findtype safefindtype = "_" if findtype == '' #the following assumes that no children are ever removed... if @cache == nil then @cache = Hash.new() end if @cache[safefindtype] == nil then @cache[safefindtype] = Hash.new() end if @cache[safefindtype][findname] == nil then self.walk() { |c| matchname = ( c.name == findname ) matchtype = ( ( c.xml_type == findtype ) or ( '' == findtype ) ) if ( matchname and matchtype ) then @cache[safefindtype][findname] = c return @cache[safefindtype][findname] end } else return @cache[safefindtype][findname] end nil end # # Find first group content node with the specified name. Returns a # subclass of Pipeline::Content::Base or nil. # def find_first_group( name ) find_first( name, 'group' ) end # # Return the Group nodes, from this group's content tree downwards, # that have the specified group name. # def find_groups( group ) find_by_script( "('group' == content.xml_type and '#{group}' == content.name)" ) end # # Return an Array of all found content nodes that cause the passed in # script to return true. Nodes are not added to our return set if the # script return's false for them. # # The script can use the keyword 'content' to mean the current content # node as we walk this content tree. # # An empty script will return no content nodes. # # E.g. # # Include all 'file' content type nodes: # find_by_script( "('file' == content.xml_type)" ) # # # Include all content types nodes: # find_by_script( "true" ) # # # Include no content type nodes: # find_by_script( "false" ) # def find_by_script( script ) throw TypeError.new( 'script must be a String' ) \ unless ( script.is_a?( String ) ) return ( [] ) if ( 0 == script.size ) return ( [] ) if ( nil == @children ) found = [] @children.each do |content| found << content if ( Kernel.eval( script ) ) # Recurse... if ( content.methods.include?( 'find_by_script' ) ) found += content.find_by_script( script ) end end found end # # Similar to 'find_by_script' except the objects in the Array are the # items returned by the script (if non-nil). # def collect_by_script( script ) throw TypeError.new( 'script must be a String' ) \ unless ( script.is_a?( String ) ) return ( [] ) if ( 0 == script.size ) return ( [] ) if ( nil == @children ) found = [] @children.each do |content| result = ( Kernel.eval( script ) ) found << result unless ( result.nil? ) # Recurse... if ( content.methods.include?( 'collect_by_script' ) ) found += content.collect_by_script( script ) end end found end end # # == Description # The Group class is a container for other content nodes (typically # the nodes are related, but that is arbitary). # class Group < Base include SupportChildren attr_reader :path # String path of content attr_writer :path XML_CONTENT_TYPE = 'group' def initialize( name, path = '' ) super( name ) init_SupportChildren( ) @xml_type = XML_CONTENT_TYPE @path = path end def to_s( ) "#{super()} path:#{@path} children(#{@children.size})" end def fill_env( env ) super( env ) env.add( 'path', @path ) end def to_xml( ) node = super( ) children.each do |child| node.add_element( child.to_xml() ) end node end # # Parse an XML node into a Group object. # def Group::from_xml( xml_node, path, env, target ) name = env.subst( xml_node.attributes['name'] ) unless ( xml_node.attributes['name'].nil? ) path = OS::Path.combine( path, xml_node.attributes['path'] ) group = Group.new( name, path ) return ( group ) end end # # == Description # Level node; equivalent to group but allows finding level directories # easier. # class Level < Group XML_CONTENT_TYPE = 'level' def initialize( name, path = '' ) super( name, path ) @xml_type = XML_CONTENT_TYPE end def Level.from_xml( xml_node, path, env, target ) name = env.subst( xml_node.attributes['name'] ) unless ( xml_node.attributes['name'].nil? ) path = OS::Path.combine( path, xml_node.attributes['path'] ) level = Level.new( name, path ) return ( level ) end end # # == Description # Template type, but masqueraded as a group so we can prevent internally # reprocessing nodes but appear to tools as a regular group. # class Template < Group def initialize( name ) super( name ) end end # # == Description # Unknown objects are created internally by the parser when an unrecognised # type string is found. # # As with Unresolved objects, these likely indicate a misconfiguration in # the content XML. # class Unknown < Group attr_reader :reason attr_reader :xml_node attr_reader :xml_filename attr_reader :xml_line attr_reader :xml_column XML_CONTENT_TYPE = 'unknown' def initialize( reason, node ) super( 'UNKNOWN' ) @xml_type = XML_CONTENT_TYPE @reason = reason @xml_node = node @xml_filename = nil @xml_line = nil @xml_column = nil end def to_s( ) # Only include the first line of XML. @xml_node =~ /(.*)$/ first_xml_line = $1 "#{super()} reason:\"#{@reason}\" xml_filename:\"#{@xml_filename}\" xml_line:#{@xml_line} xml_column:#{@xml_column} xml:#{first_xml_line}" end end end # Content module end # Pipeline module # End of treecore.rb