613 lines
16 KiB
Ruby
Executable File
613 lines
16 KiB
Ruby
Executable File
#
|
|
# File:: treecore.rb
|
|
# Description:: Content system core content tree node class implementation.
|
|
# Author:: David Muir <david.muir@rockstarnorth.com>
|
|
# Author:: Greg Smith <greg@rockstarnorth.com>
|
|
# 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
|