# The scope class, which handles storing and retrieving variables and types and
# such.

require 'puppet/parser/parser'
require 'puppet/parser/templatewrapper'
require 'puppet/transportable'
require 'strscan'

class Puppet::Parser::Scope
    require 'puppet/parser/resource'

    AST = Puppet::Parser::AST

    Puppet::Util.logmethods(self)

    include Enumerable
    include Puppet::Util::Errors
    attr_accessor :parent, :level, :interp, :source, :host
    attr_accessor :name, :type, :topscope, :base, :keyword
    attr_accessor :top, :translated, :exported, :virtual

    # Whether we behave declaratively.  Note that it's a class variable,
    # so all scopes behave the same.
    @@declarative = true

    # Retrieve and set the declarative setting.
    def self.declarative
        return @@declarative
    end

    def self.declarative=(val)
        @@declarative = val
    end

    # This handles the shared tables that all scopes have.  They're effectively
    # global tables, except that they're only global for a single scope tree,
    # which is why I can't use class variables for them.
    def self.sharedtable(*names)
        attr_accessor(*names)
        @@sharedtables ||= []
        @@sharedtables += names
    end

    # This is probably not all that good of an idea, but...
    # This way a parent can share its tables with all of its children.
    sharedtable :classtable, :definedtable, :exportable, :overridetable, :collecttable

    # Is the value true?  This allows us to control the definition of truth
    # in one place.
    def self.true?(value)
        if value == false or value == "" or value == :undef
            return false
        else
            return true
        end
    end

    # Add to our list of namespaces.
    def add_namespace(ns)
        return false if @namespaces.include?(ns)
        if @namespaces == [""]
            @namespaces = [ns]
        else
            @namespaces << ns
        end
    end

    # Is the type a builtin type?
    def builtintype?(type)
        if typeklass = Puppet::Type.type(type)
            return typeklass
        else
            return false
        end
    end

    # Create a new child scope.
    def child=(scope)
        @children.push(scope)

        # Copy all of the shared tables over to the child.
        @@sharedtables.each do |name|
            scope.send(name.to_s + "=", self.send(name))
        end
    end

    # Verify that the given object isn't defined elsewhere.
    def chkobjectclosure(obj)
        if exobj = @definedtable[obj.ref]
            typeklass = Puppet::Type.type(obj.type)
            if typeklass and ! typeklass.isomorphic?
                Puppet.info "Allowing duplicate %s" % type
            else
                # Either it's a defined type, which are never
                # isomorphic, or it's a non-isomorphic type.
                msg = "Duplicate definition: %s is already defined" % obj.ref

                if exobj.file and exobj.line
                    msg << " in file %s at line %s" %
                        [exobj.file, exobj.line]
                end

                if obj.line or obj.file
                    msg << "; cannot redefine"
                end

                raise Puppet::ParseError.new(msg)
            end
        end

        return true
    end

    # Return the scope associated with a class.  This is just here so
    # that subclasses can set their parent scopes to be the scope of
    # their parent class.
    def class_scope(klass)
        scope = if klass.respond_to?(:classname)
            @classtable[klass.classname]
        else
            @classtable[klass]
        end

        return nil unless scope

        if scope.nodescope? and ! klass.is_a?(AST::Node)
            raise Puppet::ParseError, "Node %s has already been evaluated; cannot evaluate class with same name" % [klass.classname]
        end

        scope
    end

    # Return the list of collections.
    def collections
        @collecttable
    end

    def declarative=(val)
        self.class.declarative = val
    end

    def declarative
        self.class.declarative
    end

    # Test whether a given scope is declarative.  Even though it's
    # a global value, the calling objects don't need to know that.
    def declarative?
        @@declarative
    end

    # Remove a specific child.
    def delete(child)
        @children.delete(child)
    end

    # Remove a resource from the various tables.  This is only used when
    # a resource maps to a definition and gets evaluated.
    def deleteresource(resource)
        if @definedtable[resource.ref]
            @definedtable.delete(resource.ref)
        end

        if @children.include?(resource)
            @children.delete(resource)
        end
    end

    # Are we the top scope?
    def topscope?
        @level == 1
    end

    # Return a list of all of the defined classes.
    def classlist
        unless defined? @classtable
            raise Puppet::DevError, "Scope did not receive class table"
        end
        return @classtable.keys.reject { |k| k == "" }
    end

    # Yield each child scope in turn
    def each
        @children.each { |child|
            yield child
        }
    end

    # Evaluate a list of classes.
    def evalclasses(*classes)
        retval = []
        classes.each do |klass|
            if obj = findclass(klass)
                obj.safeevaluate :scope => self
                retval << klass
            end
        end
        retval
    end

    def exported?
        self.exported
    end

    def findclass(name)
        @namespaces.each do |namespace|
            if r = interp.findclass(namespace, name)
                return r
            end
        end
        return nil
    end

    def finddefine(name)
        @namespaces.each do |namespace|
            if r = interp.finddefine(namespace, name)
                return r
            end
        end
        return nil
    end

    def findresource(string, name = nil)
        if name
            string = "%s[%s]" % [string.capitalize, name]
        end

        @definedtable[string]
    end

    # Recursively complete the whole tree, in preparation for
    # translation or storage.
    def finish
        self.each do |obj|
            obj.finish
        end
    end

    # Initialize our new scope.  Defaults to having no parent and to
    # being declarative.
    def initialize(hash = {})
        @parent = nil
        @type = nil
        @name = nil
        @finished = false
        if hash.include?(:namespace)
            if n = hash[:namespace]
                @namespaces = [n]
            end
            hash.delete(:namespace)
        else
            @namespaces = [""]
        end
        hash.each { |name, val|
            method = name.to_s + "="
            if self.respond_to? method
                self.send(method, val)
            else
                raise Puppet::DevError, "Invalid scope argument %s" % name
            end
        }

        @tags = []

        if @parent.nil?
            unless hash.include?(:declarative)
                hash[:declarative] = true
            end
            self.istop(hash[:declarative])
            @inside = nil
        else
            # This is here, rather than in newchild(), so that all
            # of the later variable initialization works.
            @parent.child = self

            @level = @parent.level + 1
            @interp = @parent.interp
            @source = hash[:source] || @parent.source
            @topscope = @parent.topscope
            #@inside = @parent.inside # Used for definition inheritance
            @host = @parent.host
            @type ||= @parent.type
        end

        # Our child scopes and objects
        @children = []

        # The symbol table for this scope.  This is where we store variables.
        @symtable = {}

        # All of the defaults set for types.  It's a hash of hashes,
        # with the first key being the type, then the second key being
        # the parameter.
        @defaultstable = Hash.new { |dhash,type|
            dhash[type] = {}
        }

        unless @interp
            raise Puppet::DevError, "Scopes require an interpreter"
        end
    end

    # Associate the object directly with the scope, so that contained objects
    # can look up what container they're running within.
    def inside(arg = nil)
        return @inside unless arg

        old = @inside
        @inside = arg
        yield
    ensure
        #Puppet.warning "exiting %s" % @inside.name
        @inside = old
    end

    # Mark that we're the top scope, and set some hard-coded info.
    def istop(declarative = true)
        # the level is mostly used for debugging
        @level = 1

        # The table for storing class singletons.  This will only actually
        # be used by top scopes and node scopes.
        @classtable = {}

        self.class.declarative = declarative

        # The table for all defined objects.
        @definedtable = {}

        # The list of objects that will available for export.
        @exportable = {}

        # The list of overrides.  This is used to cache overrides on objects
        # that don't exist yet.  We store an array of each override.
        @overridetable = Hash.new do |overs, ref|
            overs[ref] = []
        end

        # Eventually, if we support sites, this will allow definitions
        # of nodes with the same name in different sites.  For now
        # the top-level scope is always the only site scope.
        @sitescope = true

        @namespaces = [""]

        # The list of collections that have been created.  This is a global list,
        # but they each refer back to the scope that created them.
        @collecttable = []

        @topscope = self
        @type = "puppet"
        @name = "top"
    end

    # Collect all of the defaults set at any higher scopes.
    # This is a different type of lookup because it's additive --
    # it collects all of the defaults, with defaults in closer scopes
    # overriding those in later scopes.
    def lookupdefaults(type)
        values = {}

        # first collect the values from the parents
        unless @parent.nil?
            @parent.lookupdefaults(type).each { |var,value|
                values[var] = value
            }
        end

        # then override them with any current values
        # this should probably be done differently
        if @defaultstable.include?(type)
            @defaultstable[type].each { |var,value|
                values[var] = value
            }
        end

        #Puppet.debug "Got defaults for %s: %s" %
        #    [type,values.inspect]
        return values
    end

    # Look up all of the exported objects of a given type.
    def lookupexported(type)
        @definedtable.find_all do |name, r|
            r.type == type and r.exported?
        end
    end

    def lookupoverrides(obj)
        @overridetable[obj.ref]
    end

    # Look up a defined type.
    def lookuptype(name)
        finddefine(name) || findclass(name)
    end

    def lookup_qualified_var(name, usestring)
        parts = name.split(/::/)
        shortname = parts.pop
        klassname = parts.join("::")
        klass = findclass(klassname)
        unless klass
            raise Puppet::ParseError, "Could not find class %s" % klassname
        end
        unless kscope = class_scope(klass)
            raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname
        end
        return kscope.lookupvar(shortname, usestring)
    end

    private :lookup_qualified_var

    # Look up a variable.  The simplest value search we do.  Default to returning
    # an empty string for missing values, but support returning a constant.
    def lookupvar(name, usestring = true)
        # If the variable is qualified, then find the specified scope and look the variable up there instead.
        if name =~ /::/
            return lookup_qualified_var(name, usestring)
        end
        # We can't use "if @symtable[name]" here because the value might be false
        if @symtable.include?(name)
            if usestring and @symtable[name] == :undef
                return ""
            else
                return @symtable[name]
            end
        elsif self.parent 
            return @parent.lookupvar(name, usestring)
        elsif usestring
            return ""
        else
            return :undefined
        end
    end

    def namespaces
        @namespaces.dup
    end

    # Add a collection to the global list.
    def newcollection(coll)
        @collecttable << coll
    end

    # Create a new scope.
    def newscope(hash = {})
        hash[:parent] = self
        #debug "Creating new scope, level %s" % [self.level + 1]
        return Puppet::Parser::Scope.new(hash)
    end

    # Is this class for a node?  This is used to make sure that
    # nodes and classes with the same name conflict (#620), which
    # is required because of how often the names are used throughout
    # the system, including on the client.
    def nodescope?
        defined?(@nodescope) and @nodescope
    end

    # Return the list of remaining overrides.
    def overrides
        #@overridetable.collect { |name, overs| overs }.flatten
        @overridetable.values.flatten
    end

    def resources
        @definedtable.values
    end

    # Store the fact that we've evaluated a given class.  We use a hash
    # that gets inherited from the top scope down, rather than a global
    # hash.  We store the object ID, not class name, so that we
    # can support multiple unrelated classes with the same name.
    def setclass(obj)
        if obj.is_a?(AST::HostClass)
            unless obj.classname
                raise Puppet::DevError, "Got a %s with no fully qualified name" %
                    obj.class
            end
            @classtable[obj.classname] = self
        else
            raise Puppet::DevError, "Invalid class %s" % obj.inspect
        end
        if obj.is_a?(AST::Node)
            @nodescope = true
        end
        nil
    end

    # Set all of our facts in the top-level scope.
    def setfacts(facts)
        facts.each { |var, value|
            self.setvar(var, value)
        }
    end

    # Add a new object to our object table and the global list, and do any necessary
    # checks.
    def setresource(obj)
        self.chkobjectclosure(obj)

        @children << obj

        # Mark the resource as virtual or exported, as necessary.
        if self.exported?
            obj.exported = true
        elsif self.virtual?
            obj.virtual = true
        end

        # The global table
        @definedtable[obj.ref] = obj

        return obj
    end

    # Override a parameter in an existing object.  If the object does not yet
    # exist, then cache the override in a global table, so it can be flushed
    # at the end.
    def setoverride(resource)
        resource.override = true
        if obj = @definedtable[resource.ref]
            obj.merge(resource)
        else
            @overridetable[resource.ref] << resource
        end
    end

    # Set defaults for a type.  The typename should already be downcased,
    # so that the syntax is isolated.  We don't do any kind of type-checking
    # here; instead we let the resource do it when the defaults are used.
    def setdefaults(type, params)
        table = @defaultstable[type]

        # if we got a single param, it'll be in its own array
        params = [params] unless params.is_a?(Array)

        params.each { |param|
            #Puppet.debug "Default for %s is %s => %s" %
            #    [type,ary[0].inspect,ary[1].inspect]
            if @@declarative
                if table.include?(param.name)
                    self.fail "Default already defined for %s { %s }" %
                            [type,param.name]
                end
            else
                if table.include?(param.name)
                    # we should maybe allow this warning to be turned off...
                    Puppet.warning "Replacing default for %s { %s }" %
                        [type,param.name]
                end
            end
            table[param.name] = param
        }
    end

    # Set a variable in the current scope.  This will override settings
    # in scopes above, but will not allow variables in the current scope
    # to be reassigned if we're declarative (which is the default).
    def setvar(name,value, file = nil, line = nil)
        #Puppet.debug "Setting %s to '%s' at level %s" %
        #    [name.inspect,value,self.level]
        if @symtable.include?(name)
            if @@declarative
                error = Puppet::ParseError.new("Cannot reassign variable %s" % name)
                if file
                    error.file = file
                end
                if line
                    error.line = line
                end
                raise error
            else
                Puppet.warning "Reassigning %s to %s" % [name,value]
            end
        end
        @symtable[name] = value
    end

    # Return an interpolated string.
    def strinterp(string, file = nil, line = nil)
        # Most strings won't have variables in them.
        ss = StringScanner.new(string)
        out = ""
        while not ss.eos?
            if ss.scan(/^\$\{((\w*::)*\w+)\}|^\$((\w*::)*\w+)/) 
                # If it matches the backslash, then just retun the dollar sign.
                if ss.matched == '\\$'
                    out << '$'
                else # look the variable up
                    out << lookupvar(ss[1] || ss[3]).to_s || ""
                end
            elsif ss.scan(/^\\(.)/)
                # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched])
                case ss[1]
                when 'n'
                    out << "\n"
                when 't'
                    out << "\t"
                when 's'
                    out << " "
                when '\\'
                    out << '\\'
                when '$'
                    out << '$'
                else
                    str = "Unrecognised escape sequence '#{ss.matched}'"
                    if file
                        str += " in file %s" % file
                    end
                    if line
                        str += " at line %s" % line
                    end
                    Puppet.warning str
                    out << ss.matched
                end
            elsif ss.scan(/^\$/)
                out << '$'
            elsif ss.scan(/^\\\n/) # an escaped carriage return
                next
            else 
                tmp = ss.scan(/[^\\$]+/)
                # Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp])
                unless tmp
                    error = Puppet::ParseError.new("Could not parse string %s" %
                        string.inspect)
                    {:file= => file, :line= => line}.each do |m,v|
                        error.send(m, v) if v
                    end
                    raise error
                end
                out << tmp
            end
        end

        return out
    end

    # Add a tag to our current list.  These tags will be added to all
    # of the objects contained in this scope.
    def tag(*ary)
        ary.each { |tag|
            if tag.nil? or tag == ""
                puts caller
                Puppet.debug "got told to tag with %s" % tag.inspect
                next
            end
            unless tag =~ /^\w[-\w]*$/
                fail Puppet::ParseError, "Invalid tag %s" % tag.inspect
            end
            tag = tag.to_s
            unless @tags.include?(tag)
                #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag]
                @tags << tag
            end
        }
    end

    # Return the tags associated with this scope.  It's basically
    # just our parents' tags, plus our type.  We don't cache this value
    # because our parent tags might change between calls.
    def tags
        tmp = [] + @tags
        unless ! defined? @type or @type.nil? or @type == ""
            tmp << @type.to_s
        end
        if @parent
            #info "Looking for tags in %s" % @parent.type
            @parent.tags.each { |tag|
                if tag.nil? or tag == ""
                    Puppet.debug "parent returned tag %s" % tag.inspect
                    next
                end
                unless tmp.include?(tag)
                    tmp << tag
                end
            }
        end
        return tmp.sort.uniq
    end

    # Used mainly for logging
    def to_s
        if self.name
            return "%s[%s]" % [@type, @name]
        else
            return self.type.to_s
        end
    end

    # Convert all of our objects as necessary.
    def translate
        ret = @children.collect do |child|
            case child
            when Puppet::Parser::Resource
                child.to_trans
            when self.class
                child.translate
            else
                devfail "Got %s for translation" % child.class
            end
        end.reject { |o| o.nil? }
        bucket = Puppet::TransBucket.new ret

        case self.type
        when "": bucket.type = "main"
        when nil: devfail "A Scope with no type"
        else
            bucket.type = @type
        end
        if self.name
            bucket.name = self.name
        end
        return bucket
    end

    # Undefine a variable; only used for testing.
    def unsetvar(var)
        if @symtable.include?(var)
            @symtable.delete(var)
        end
    end

    # Return an array of all of the unevaluated objects
    def unevaluated
        ary = @definedtable.find_all do |name, object|
            ! object.builtin? and ! object.evaluated?
        end.collect { |name, object| object }

        if ary.empty?
            return nil
        else
            return ary
        end
    end

    def virtual?
        self.virtual || self.exported?
    end
end

# $Id: scope.rb 2646 2007-07-04 21:06:26Z luke $


syntax highlighted by Code2HTML, v. 0.9.1