# = XmlConfigFile
#
# Author:: Maik Schmidt (mailto:contact@maik-schmidt.de)
# Copyright:: Copyright (c) 2003,2004 Maik Schmidt
# License:: Distributes under the same terms as Ruby
require 'observer'
require 'thread'
require 'rexml/document'
require 'rexml/xpath'
# This class represents an XML configuration file as it is
# used by many modern applications. Using this class you
# can access an XML file's content easily using XPath.
class XmlConfigFile
include Observable
include REXML
DEFAULT_SEPARATOR = "."
DEFAULT_TRUE_VALUES = ['1', 'yes', 'true', 'on']
DEFAULT_FALSE_VALUES = ['0', 'no', 'false', 'off']
attr_reader :filename, :true_values, :false_values, :doc
attr_accessor :expand_attributes, :path_prefix
attr_accessor :no_hash_key_element_path
# Conversion of DEFAULT_TRUE_VALUES to Ruby Boolean
# objects, everything else to strings.
attr_accessor :auto_convert_boolean
# Conversion of any node text to Ruby Object attempted
# by evaluation, otherwise returns string value of node.
attr_accessor :auto_convert_any
# Creates and initializes a new XmlConfigFile object.
#
# filename::
# Name of the configuration file to be loaded.
# reloadPeriod::
# If the configuration file should be reloaded periodically,
# set this parameter to a numeric value greater than zero,
# which specifies the reload period measured in seconds.
#
# The following exceptions may be raised:
#
# Errno::ENOENT::
# If the specified file does not exist.
# REXML::ParseException::
# If the specified file is not wellformed.
# ArgumentError::
# If the reload period is not a number greater than zero.
def initialize(filename, reloadPeriod = nil)
@filename = filename
@fileaccess = Mutex.new
@true_values = DEFAULT_TRUE_VALUES
@false_values = DEFAULT_FALSE_VALUES
@expand_attributes = false
@path_prefix = ''
@doc = load_configuration_file(@filename)
@reloaderThread = initialize_reloader_thread(reloadPeriod)
end
# Returns a configuration parameter as an Object. The
# parameter itself has to be identified by an XPath
# expression.
#
# In the event that an Object isn't created it will
# return a string
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as String value.
# default::
# Default value to be returned, if the XPath expression
# returned an empty node list. Of course, the default
# value should be a String value in such a case. The
# method does not check this explicitly.
def get_object(xpath, default = nil)
parameter = XPath.first(@doc, @path_prefix + xpath)
return node_to_object(parameter, default)
end
alias_method :getObject, :get_object
alias_method :getParameterObject, :get_object
# Returns a configuration parameter as a String. The
# parameter itself has to be identified by an XPath
# expression.
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as String value.
# default::
# Default value to be returned, if the XPath expression
# returned an empty node list. Of course, the default
# value should be a String value in such a case. The
# method does not check this explicitly.
def get_string(xpath, default = nil)
parameter = XPath.first(@doc, @path_prefix + xpath)
return node_to_text(parameter, default)
end
alias_method :[], :get_string
alias_method :getParameter, :get_string
alias_method :get_parameter, :get_string
# Sets a configuration parameter. The parameter has to
# be specified by an XPath expression.
# If the parameter was set successfully, all observers
# will be notified.
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as String value.
# value::
# The parameter value to be set.
# create::
# If the node specified by xpath does not exist, it
# will be created, if this parameter is set to true.
# Otherwise, an exception will be thrown.
def set_parameter(xpath, value, create = false)
parameter = XPath.first(@doc, xpath)
if parameter.nil?
if create
#
# Re-Call the set method if the create succeeds
#
return set_parameter(xpath,value) if create_path(xpath)
# raise ArgumentError, "Not yet implemented!"
end
raise ArgumentError , "XPath Doesn't Exist :: Create is #{create}"
elsif parameter.instance_of?(Element)
unless value.nil?
parameter.text = value.to_s
else
parameter.text = nil
end
elsif parameter.instance_of?(Attribute)
parameter.element.add_attribute(parameter.name, value.to_s)
end
changed
notify_observers(@filename)
end
alias_method :[]=, :set_parameter
#
# Add a value according to an xpath. Parents will be created if they don't
# exist. This is set_parameter with the create flag set to true
#
def add_parameter(xpath,value)
set_parameter(xpath,value,true)
end
# Returns a configuration parameter as an Integer. The
# parameter itself has to be identified by an XPath
# expression.
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as Integer value.
# default::
# Default value to be returned, if the XPath expression
# returned an empty node list. Of course, the default
# value should be an Integer value in such a case. The
# method does not check this explicitly.
#
# The following exception may be raised:
#
# ArgumentError::
# If a parameter was found and could not be converted
# into an Integer.
def get_int(xpath, default = nil)
parameter = get_string(xpath)
parameter ? Integer(parameter) : default
end
alias_method :getIntParameter, :get_int
alias_method :get_int_parameter, :get_int
# Returns a configuration parameter as a Float. The
# parameter itself has to be identified by an XPath
# expression.
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as Float value.
# default::
# Default value to be returned, if the XPath expression
# returned an empty node list. Of course, the default
# value should be a Float value in such a case. The
# method does not check this explicitly.
#
# The following exception may be raised:
#
# ArgumentError::
# If a parameter was found and could not be converted
# into a Float.
def get_float(xpath, default = nil)
parameter = get_string(xpath)
parameter ? Float(parameter) : default
end
alias_method :getFloatParameter, :get_float
alias_method :get_float_parameter, :get_float
# Returns a configuration parameter as a boolean. The
# parameter itself has to be identified by an XPath
# expression.
#
# By default, the values in DEFAULT_TRUE_VALUES are
# assumed to be true and the values in DEFAULT_FALSE_VALUES
# are assumed to be false. You can define your own
# values by setting true_values respectively false_values
# to an array containing the values of your choice.
#
# xpath::
# An XPath expression identifying the configuration
# parameter to be read and returned as boolean value.
# default::
# Default value to be returned, if the XPath expression
# returned an empty node list. Of course, the default
# value should be a boolean value in such a case. The
# method does not check this explicitly.
#
# The following exception may be raised:
#
# ArgumentError::
# If a parameter was found and could not be converted
# into a boolean.
def get_boolean(xpath, default = nil)
parameter = get_string(xpath)
return default unless parameter
value = convert_boolean(parameter)
return value unless value.nil?
raise ArgumentError
end
alias_method :getBooleanParameter, :get_boolean
alias_method :get_boolean_parameter, :get_boolean
# Returns a set of String parameters as a one-dimensional
# Hash.
# If no parameters specified by xpath were found, an
# empty Hash will be returned.
#
# E.g. the document snippet
#
# ...
#
# shop
# scott
#
# ...
#
# will become the following Hash
#
# { db.name => shop, db.user => scott }
#
# xpath::
# An XPath expression specifying the nodes to be selected.
# pathSeparator::
# String separating the single path elements in the Hash keys.
def get_parameters(xpath, pathSeparator = DEFAULT_SEPARATOR)
parameters = Hash.new
XPath.each(@doc, xpath) { |node|
parameters.update(get_properties(node, pathSeparator))
}
return parameters
end
alias_method :getParameters, :get_parameters
# Returns an array of Hashes. Each element contains a set of String
# parameters as Hash.
# If no parameters specified by xpath were found, an empty Array
# will be returned.
#
# E.g. the document snippet
#
# ...
#
# shop
# scott
#
#
# factory
# anna
#
# ...
#
# will become the following Array
#
# [{ db.name => shop, db.user => scott },
# { db.name => factory, db.user => anna }]
#
# This method was originally contributed by Nigel Ball.
#
# xpath::
# An XPath expression specifying the nodes to be selected.
# pathSeparator::
# String separating the single path elements in the Hash keys.
def get_string_array(xpath, pathSeparator = DEFAULT_SEPARATOR, expand = false)
old_expand_attributes = @expand_attributes
@expand_attributes = expand
parameter_array = Array.new
XPath.each(@doc, xpath) { |node|
parameter_array << get_properties(node, pathSeparator)
}
@expand_attributes = old_expand_attributes
return parameter_array
end
alias_method :get_parameter_array, :get_string_array
# Sets the list of values, that will be interpreted as true
# boolean parameters. The case of all the elements will be
# set to downcase and all leading and trailing whitespaces
# will be removed.
#
# values::
# List of values to be interpreted as true boolean values.
def true_values=(values)
@true_values = values.collect { |x| x.strip.downcase }
end
# Sets the list of values, that will be interpreted as false
# boolean parameters. The case of all the elements will be
# set to downcase and all leading and trailing whitespaces
# will be removed.
#
# values::
# List of values to be interpreted as false boolean values.
def false_values=(values)
@false_values = values.collect { |x| x.strip.downcase }
end
# Stores the current configuration into a file.
#
# filename::
# Name of the file to store configuration in. Defaults
# to the current configuration file's name.
def store(filename = nil)
filename = @filename if filename.nil?
@fileaccess.synchronize {
File.open(filename, "w") { |f| f.write(to_s) }
}
end
# Returns the current configuration as an XML string.
def to_s
content = ''
@doc.write(content, 0)
return content
#return @doc.to_s
end
# Closes the configuration file, i.e. stops the reloader
# thread.
def close
@reloaderThread.exit unless @reloaderThread.nil?
@reloaderThread = nil
end
# Converts the current configuration into a Hash in the same
# way as the Perl module XML::Simple.
def to_hash
end
private
# Initializes and returns a reloader thread, that will reload
# the configuration file periodically (only if it has changed,
# of course). If the configuration was reloaded, all observers
# will be notified.
# If the configuration file to be reloaded is not
# wellformed or could not be found, an error message
# will be printed to standard error and the last
# working configuration will be used.
#
# reloadPeriod::
# Reload period measured in seconds.
#
# The following exception may be raised:
#
# ArgumentError::
# If the reload period is not a number greater than zero.
def initialize_reloader_thread(reloadPeriod)
return nil if reloadPeriod.nil?
if !reloadPeriod.kind_of?(Integer) && !reloadPeriod.kind_of?(Float)
raise ArgumentError
elsif reloadPeriod <= 0
raise ArgumentError
end
@lastModificationTime = File::mtime(@filename).to_i
return Thread.new {
loop do
sleep(reloadPeriod)
begin
currentModificationTime = File::mtime(@filename).to_i
if currentModificationTime != @lastModificationTime
@lastModificationTime = currentModificationTime
@fileaccess.synchronize {
currentConfiguration = load_configuration_file(@filename)
@doc = currentConfiguration
}
changed
notify_observers(@filename)
end
rescue Exception => ex
$stderr.puts("Invalid configuration in '#{@filename}':", ex)
end
end
}
end
# Loads and parses an XML configuration file. Additionally,
# references to environment variables will be replaced by
# their actual values.
#
# filename::
# Name of the configuration file to be loaded.
# substituteVariables::
# Indicates, if references to environment variables should
# be replaced by their values.
#
# The following exceptions may be raised:
#
# Errno::ENOENT::
# If the specified file does not exist.
# REXML::ParseException::
# If the specified file is not wellformed.
def load_configuration_file(filename, substituteVariables = true)
xmlstring = File.readlines(filename).to_s
doc = Document.new(xmlstring)
substitute_environment_variables(doc) if substituteVariables
return doc
end
# Replaces all references to environment variables in a
# String by their actual values.
#
# text::
# A String containing references to environment
# variables to be replaced.
def substitute_environment_variable(text)
text.gsub!(/\$\{(\w+)\}/) { |s| ENV[$1] ? ENV[$1] : "" }
text
end
# Replaces all references to environment variables in a
# document with their actual values.
#
# doc::
# Document containing references to be replaced.
def substitute_environment_variables(doc)
XPath.each(doc, '//*') { |element|
if element.instance_of?(Element)
if element.text
element.text = substitute_environment_variable(element.text)
end
element.attributes.each { |name, value|
substitute_environment_variable(value)
}
end
}
end
# Converts a document node into a String.
# If the node could not be converted into a String
# for any reason, default will be returned.
#
# node::
# Document node to be converted.
# default::
# Value to be returned, if node could not be converted.
def node_to_text(node, default = nil)
if node.instance_of?(Element)
# Bug fix provided by Sandra Silcot.
return node.text.nil? ? default : node.text.strip
elsif node.instance_of?(Attribute)
return node.value.nil? ? default : node.value.strip
elsif node.instance_of?(Text)
return node.to_s.strip
else
return default
end
end
# Converts a document node into a Ruby object.
#
# If :auto_convert_boolean is set then String
# objects matching DEFAULT_FALSE_VALUES or DEFAULT_TRUE_VALUES
# will be converted to a Ruby Boolean Object.
#
# If :auto_convert_any is set then String objects
# will be attempted to be converted to evaluated values.
# DEFAULT_[TRUE|FALSE]_VALUES will be ignored.
#
# "1" becomes FixNum instance
# "true" becomes TrueClass instance
# "blah" becomes String instance
def conditional_node_to_object(node, default = nil)
if self.auto_convert_any
return node_to_object(node,default)
elsif self.auto_convert_boolean
return node_to_boolean(node,default)
else
return node_to_text(node,default)
end
end
# Converts a document node into a Boolean.
#
# If matches against DEFAULT_TRUE_VALUES 'true' is returned.
# If matches against DEFAULT_FALSE_VALUE 'false is returned.
#
# If the Node cannot be converted to Boolean, then it
# will return the node as it would have been if it was
# converted to a String
def node_to_boolean(node, default = nil)
nodeText = node_to_text(node,default)
value = convert_boolean(nodeText)
return value unless value.nil?
return nodeText
end
# Converts a document node into a a native ruby object.
#
# If auto_convert_boolean is on, this
# method will defer to the DEFAULT_TRUE_VALLUES
# and DEFAULT_FALSE_VALUES for conversion.
#
# This will cause behaviour like '1' and '0' to become
# 'true', 'false' instead of FixNum instance 1 and 0.
#
# Attempts to evaluate the nodeText, if it cannot it will
# just return the node's text.
def node_to_object(node, default = nil)
nodeText = node_to_text(node,default)
retried = false
if self.auto_convert_boolean
value = convert_boolean(nodeText)
return value unless value.nil?
end
if not nodeText.nil?
evalString = nodeText
else
evalString = ''
end
begin
val = eval(evalString)
rescue SyntaxError, NameError, TypeError
# Would like to find out what all the possible
# errors of eval would be.
#
# Curtis Says:
#
# This is probably an ugly hack.
#
# Uncertain if trying lowercase will
# have an unwanted effect. This is
# for 'TrUe' to evaluate to true::TrueClass
if not retried
retried = true
evalString = nodeText.downcase
retry
end
val = nodeText
end
val
end
# Converts a string into Boolean object,
#
# Returns nil on un-success.
def convert_boolean(parameter)
unless parameter.nil?
lowParam = parameter.downcase
return true if @true_values.include?(lowParam)
return false if @false_values.include?(lowParam)
end
return nil
end
# Returns the path to a node as String. All elements
# will be delimited by the string pathSeparator.
#
# node::
# Node to return path for.
# pathSeparator::
# String separating the single path elements in
# the Hash keys.
def get_path(node, pathSeparator)
unless self.no_hash_key_element_path
if node.parent == @doc.root
return node.name
else
return get_path(node.parent, pathSeparator) + pathSeparator + node.name
end
else
return node.name
end
end
# Converts a document node into a Hash object.
#
# node::
# Node to be converted into a Hash.
# pathSeparator::
# String separating the single path elements in the Hash keys.
def get_properties(node, pathSeparator)
parameters = Hash.new
if node.instance_of?(Element)
if !node.has_elements?
parameters[get_path(node, pathSeparator)] = conditional_node_to_object(node)
else
node.each_element { |child|
parameters.update(get_properties(child, pathSeparator))
}
end
if @expand_attributes
node.attributes.each { |name, value|
parameters[get_path(node, pathSeparator) + pathSeparator + name] = value
}
end
end
return parameters
end
#
# Creates a specified XPath.
#
# Currently only supports absolute paths, non-indexed (first match)
#
#
# xpath::
# An XPath expression identifying a complete path of nodes or attributes that
# should exist in the xml document after the execution of this method.
#
# Return true if success
#
# The following Exceptions may be thrown
#
# ArgumentError::
# when unsupported features of xpath attempt to be created
#
def create_path(xpath)
#
# Iterate frontwards through each portion of the xpath
#
#
current_xpath =""
#
# xml/xxpath.rb influenced the idea to use a case with
# regular expressions to match against the different part "types"
#
xpath.split('/').each do | part |
unless part.to_s.empty?
last_existing_node = XPath.first(@doc,current_xpath)
#
# This should always set the last_existing path
#
unless last_existing_node.nil?
last_existing_path = current_xpath
else
raise "AssertFailed :: LastExisiting Path should never be Invalid! -- #{current_xpath}"
end
current_xpath << '/' << part
current_node = XPath.first(@doc,current_xpath)
#
# Check to see if this part of the path exists
#
# If it doesn't go through the process to create it
#
# Otherwise move onto the next portion of the parent
if current_node.nil?
case part
when /^@(\w+)$/
attr_name = $1
create_attribute_at(last_existing_node,attr_name)
when /^(\w+)$/
element_name = $1
create_element_at(last_existing_node,element_name)
else
raise ArgumentError.new("Unsupported XPath Creation Part : #{part} ")
end
end
end
end
(xpath == current_xpath)
end
#http://www.ruby-doc.org/stdlib/libdoc/rexml/rdoc/classes/REXML/Element.html
#
# Create an attribute in the REXML document
#
def create_attribute_at(node,attribute_name, value = "")
node.add_attribute(attribute_name,value)
end
#
# create a node in the XML Document
#
def create_element_at(node,element_name)
node.add_element(element_name)
end
end