# Simple Declarative Language (SDL) for Ruby # Copyright 2005 Ikayzo, inc. # # This program is free software. You can distribute or modify it under the # terms of the GNU Lesser General Public License version 2.1 as published by # the Free Software Foundation. # # This program is distributed AS IS and WITHOUT WARRANTY. OF ANY KIND, # INCLUDING MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. # See the GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, contact the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. module SDL4R require 'pathname' require 'open-uri' require 'stringio' require File.dirname(__FILE__) + '/sdl' require File.dirname(__FILE__) + '/parser' # SDL (Simple Declarative Language) documents are made up of Tags. A Tag # contains # # * a name (if not present, the name "content" is used) # * a namespace (optional) # * 0 or more values (optional) # * 0 or more attributes (optional) # * 0 or more children (optional) # # For the SDL code: # # size 4 # smoker false # # # Assuming this code is in a file called values.sdl, the values can be read # using the following code (ignoring exceptions): # # root = Tag.new("root").read(new File("values.sdl")); # int size = root.getChild("size").intValue(); # boolean smoker = root.getChild("smoker").booleanValue(); # # A tag is basically a data structure with a list of values, a map of # attributes, and (if it has a body) child tags. In the example above, the # "values.sdl" file is read into a tag called "root". It has two children # (tags) called "size" and "smoker". Both these children have one value, no # attributes, and no bodies. # # SDL is often used for simple key-value mappings. To simplify things Tag # has the methods getValue and setValue which operate on the first element in # the values list. Also notice SDL understands types which are determined # using type inference. # # The example above used the simple format common in property files: # # name value # # The full SDL tag format is: # # namespace:name value_list attribute_list { # children_tags # } # # where value_list is zero or more space separated SDL literals and # attribute_list is zero or more space separated (namespace:)key=value pairs. # The name, namespace, and keys are SDL identifiers. Values are SDL literals. # Namespace is optional for both tag names and attributes. Tag bodies are also # optional. SDL identifiers begin with a unicode letter or an underscore (_) # followed by zero or more unicode letters, numbers, underscores (_) and # dashes (-). # # SDL also supports anonymous tags which are assigned the name "content". # An annoymous tag starts with a literal and is followed by zero or more # additional literals and zero or more attributes. The examples section below # demonstrates the use of anonymous tags. # # Tags without bodies are terminated by a new line character (\n) and may be # continue onto the next line by placing a backslash (\) at the end of the # line. Tags may be nested to an arbitrary depth. SDL ignores all other white # space characters between tokens. Although nested blocks are indented by # convention, tabs have no significance in the language. # # There are two ways to write String literals. # # 1. Starting and ending with double quotes ("). Double quotes, backslash # characters (\), and new lines (\n) within this type of String literal # must be escaped like so: # # file "C:\\folder\\file.txt" # say "I said \"something\"" # # This type of String literal can be continued on the next line by placing a # backslash (\) at the end of the line like so: # # line "this is a \ # long string of text" # # White space before the first character in the second line will be ignored. # # 2. Starting and ending with a backquote (`). This type of string literal # can only be ended with a second backquote (`). It is not necessary (or # possible) to escape any type of character within a backquote string # literal. This type of literal can also span lines. All white spaces are # preserved including new lines. # # Examples: # # file `C:\folder\file.txt` # say `I said "something"` # regex `\w+\.suite\(\)` # long_line `This is # a long line # fee fi fo fum` # # Note: SDL interprets new lines in `` String literals as a single new line # character (\n) regarless of the platform. # # Binary literals use base64 characters enclosed in square brackets ([]). # The binary literal type can also span lines. White space is ignored. # # # Examples: # key [sdf789GSfsb2+3324sf2] name="my key" # image [ # R3df789GSfsb2edfSFSDF # uikuikk2349GSfsb2edfS # vFSDFR3df789GSfsb2edf # ] # upload from="ikayzo.com" data=[ # R3df789GSfsb2edfSFSDF # uikuikk2349GSfsb2edfS # vFSDFR3df789GSfsb2edf # ] # # SDL supports date, time span, and date/time literals. Date and Date/Time # literals use a 24 hour clock (0-23). If a timezone is not specified, the # default locale's timezone will be used. # # Examples: # # # create a tag called "date" with a date value of Dec 5, 2005 # date 2005/12/05 # # # various time span literals # hours 03:00:00 # minutes 00:12:00 # seconds 00:00:42 # short_time 00:12:32.423 # 12 minutes, 32 seconds, 423 milliseconds # long_time 30d:15:23:04.023 # 30 days, 15 hours, 23 mins, 4 secs, 23 millis # before -00:02:30 # 2 hours and 30 minutes ago # # # a date time literal # in_japan 2005/12/05 14:12:23.345-JST # # # SDL 1.0 has thirteen literal types (parenthesis indicate optional components) # # 1. string (unicode) - examples: "hello" or `aloha` # 2. character (unicode) - example: '/' # Note: \uXXXX style unicode escapes are not supported (or # needed because sdl files are UTF8) # 3. integer (32 bits signed) - example: 123 # 4. long integer (64 bits signed) - examples: 123L or 123l # 5. float (32 bits signed) - examples 123.43F 123.43f # 6. double float (64 bits signed) - example: 123.43 or 123.43d or 123.43D # 7. decimal (128+ bits signed) - example: 123.44BD or 123.44bd # 8. boolean - examples: true or false or on or off # 9. date yyyy/mm/dd - example 2005/12/05 # 10. date time yyyy/mm/dd hh:mm(:ss)(.xxx)(-ZONE) # example - 2005/12/05 05:21:23.532-JST # notes: uses a 24 hour clock (0-23), only hours and minutes are # mandatory # 11. time span using the format (d:)hh:mm:ss(.xxx) # notes: if the day component is included it must be suffixed with # a lower case 'd' # examples 12:14:42 (12 hours, 14 minutes, 42 seconds) # 00:09:12 (9 minutes, 12 seconds) # 00:00:01.023 (1 second, 23 milliseconds) # 23d:05:21:23.532 (23 days, 5 hours, 21 minutes, # 23 seconds, 532 milliseconds) # 12. binary [base64] exmaple - [sdf789GSfsb2+3324sf2] # 13. null # # # Timezones must be specified using a valid time zone ID (ex. America/Los_Angeles), three letter # abbreviation (ex. HST), or GMT(+/-)hh(:mm) formatted custom timezone (ex. GMT+02 or GMT+02:30) # # Note: SDL 1.1 will likely add a reference literal type. # # These types are designed to be portable across Java, .NET, and other # popular platforms. # # # SDL supports four comment types. # # 1. // single line comments identicle to those used in Java, C, etc. // style # comments can occur anywhere in a line. All text after // up to the new line # will be ignored. # 2. # property style comments. They work the same way as // # 3. -- separator comments useful for visually dividing content. They work the same way as // # 4. Slash star (/*) style multiline comments. These begin with a slash # star and end with a star slash. Everything in between is ignored. # # # # An example SDL file: # # # a tag having only a name # my_tag # # # three tags acting as name value pairs # first_name "Akiko" # last_name "Johnson" # height 68 # # # a tag with a value list # person "Akiko" "Johnson" 68 # # # a tag with attributes # person first_name="Akiko" last_name="Johnson" height=68 # # # a tag with values and attributes # person "Akiko" "Johnson" height=60 # # # a tag with attributes using namespaces # person name:first-name="Akiko" name:last-name="Johnson" # # # a tag with values, attributes, namespaces, and children # my_namespace:person "Akiko" "Johnson" dimensions:height=68 { # son "Nouhiro" "Johnson" # daughter "Sabrina" "Johnson" location="Italy" { # hobbies "swimming" "surfing" # languages "English" "Italian" # smoker false # } # } # # ------------------------------------------------------------------ # // (notice the separator style comment above...) # # # a log entry # # note - this tag has two values (date_time and string) and an # # attribute (error) # entry 2005/11/23 10:14:23.253-GMT "Something bad happened" error=true # # # a long line # mylist "something" "another" true "shoe" 2002/12/13 "rock" \ # "morestuff" "sink" "penny" 12:15:23.425 # # # a long string # text "this is a long rambling line of text with a continuation \ # and it keeps going and going..." # # # anonymous tag examples # # files { # "/folder1/file.txt" # "/file2.txt" # } # # # To retrieve the files as a list of strings # # # # List files = tag.getChild("files").getChildrenValues("content"); # # # # We us the name "content" because the files tag has two children, each of # # which are anonymous tags (values with no name.) These tags are assigned # # the name "content" # # matrix { # 1 2 3 # 4 5 6 # } # # # To retrieve the values from the matrix (as a list of lists) # # # # List rows = tag.getChild("matrix").getChildrenValues("content"); # # # Example of getting the "location" attribute from the "daughter" tag # above (ignoring exceptions) # # Tag root = new Tag("root").read("myfile.sdl"); # Tag daughter = root.getChild("daughter", true); // recursive search # String location = daughter.getAttribute("location").toString(); # # SDL is normally stored in a file with the .sdl extension. These files # should always be encoded using UTF8. SDL fully supports unicode in # identifiers and literals. # # @author Daniel Leuck # class Tag # # the name of this Tag # attr_reader :name # # the namespace of this Tag or an empty string when there is no namespace. # attr_reader :namespace # Creates an empty tag in the given namespace. If the +namespace+ is null # it will be coerced to an empty String. # # +namespace+:: the namespace for this tag # +name+:: the name of this tag # # Throws ArgumentError if the name is not a legal SDL identifier # (see SDL#validate_identifier) or the namespace is non-blank # and is not a legal SDL identifier. # def initialize(name, namespace = "", &block) namespace = namespace.to_s SDL4R.validate_identifier(namespace) unless namespace.empty? @namespace = namespace name = name.to_s.strip raise ArgumentError, "Tag name cannot be null or empty" if name.empty? SDL4R.validate_identifier(name) @name = name @children = [] @values = [] # a Hash of Hash : {namespace => {name => value}} # The default namespace is represented by an empty string. @attributesByNamespace = {} instance_eval(&block) if block_given? end # Creates a new child tag. # Can take a block so that you can write something like: # # car = Tag.new("car") do # new_child("wheels") do # self << 4 # end # end # # The context of execution of the given block is the child instance # # Returns the created child Tag. # def new_child(*args, &block) return add_child Tag.new(*args, &block) end # Add a child to this Tag. # # +child+:: The child to add # # Returns the added child. # def add_child(child) @children.push(child) return child end # Adds the given object as a child if it is a +Tag+, as an attribute if it is a Hash # {key => value} (supports namespaces), or as a value otherwise. # # Returns +self+. # def <<(o) if o.is_a?(Tag) add_child(o) elsif o.is_a?(Hash) o.each_pair { |key, value| namespace, key = key.split(/:/) if key.match(/:/) namespace ||= "" set_attribute(key, value, namespace) } else add_value(o) end return self end # Remove a child from this Tag # # +child+:: the child to remove # # Returns true if the child exists and is removed # def remove_child(child) return !@children.delete(child).nil? end # Removes all children. # def clear_children @children = [] end # # A convenience method that sets the first value in the value list. See # {@link #addValue(Object)} for legal types. # # +value+:: The value to be set. # @throws IllegalArgumentException if the value is not a legal SDL type # def value=(value) @values[0] = SDL4R.coerce_or_fail(value) end # # A convenience method that returns the first value. # def value @values[0] end # Returns the number of children Tag. # def child_count @children.size end # Returns an Array of the children Tags of this Tag. # # +recursive+:: if true children and all descendants will be returned. False by default. # +name+:: if not nil, only children having this name will be returned. Nil by default. # +namespace+:: if not nil, only children having this namespace will be returned. # Nil by default. # def children(recursive = false, name = nil, namespace = nil, &block) if block_given? each_child(recursive, name, namespace, &block) else unless recursive or name or namespace @children else result = [] each_child(recursive, name, namespace) { |child| result << child } return result end end end # Returns the values of all the children with the given +name+. If the child has # more than one value, all the values will be added as a list. If the child # has no value, +nil+ will be added. The search is not recursive. # # +name+:: if nil, all children are considered (nil by default). def children_values(name = nil) children_values = [] each_child(false, name) { |child| case child.values.size when 0 children_values << nil when 1 children_values << child.value else children_values << child.values end } return children_values end # Get the first child with the given name, optionally using a recursive search. # # +name+:: the name of the child Tag. If +nil+, the first child is returned (+nil+ if there are # no children at all). # # Returns the first child tag having the given name or +nil+ if no such child exists # def child(name = nil, recursive = false) unless name return @children.first else each_child(recursive, name) { |child| return child } end end # Indicates whether the child Tag of given name exists. # # +name+:: name of the searched child Tag # def has_child?(name) !child(name).nil? end # Indicates whether there are children Tag. # def has_children? !@children.empty? end # Enumerates the children +Tag+s of this Tag and calls the given block # providing it the child as parameter. # # +recursive+:: if true, enumerate grand-children, etc, recursively # +name+:: if not nil, indicates the name of the children to enumerate # +namespace+:: if not nil, indicates the namespace of the children to enumerate # def each_child(recursive = false, name = nil, namespace = nil, &block) @children.each do |child| if (name.nil? or child.name == name) and (namespace.nil? or child.namespace == namespace) yield child end child.children(recursive, name, namespace, &block) if recursive end return nil end private :each_child # Returns a new Hash where the children's names as keys and their values as the key's value. # Example: # # child1 "toto" # child2 2 # # would give # # { "child1" => "toto", "child2" => 2 } # def to_child_hash hash = {} children { |child| hash[child.name] = child.value } return hash end # Returns a new Hash where the children's names as keys and their values as the key's value. # Values are converted to Strings. +nil+ values become empty Strings. # Example: # # child1 "toto" # child2 2 # child3 null # # would give # # { "child1" => "toto", "child2" => "2", "child3" => "" } # def to_child_string_hash hash = {} children do |child| # FIXME: it is quite hard to be sure whether we should mimic the Java version # as there might be a lot of values that don't translate nicely to Strings. hash[child.name] = child.value.to_s end return hash end # Adds a value to this Tag. The allowable types are String, Number, # Boolean, Character, byte[], Byte[] (coerced to byte[]), Calendar, # Date (coerced to Calendar), and null. Passing any other type will # result in an IllegalArgumentException. # # +v+:: The value to add # # Raises a +ArgumentError+ if the value is not a legal SDL type # def add_value(v) @values.push(SDL4R::coerce_or_fail(v)) end # Returns true if +v+ is a value of this Tag's. # def has_value?(v) @values.include?(v) end # Remove a value from this Tag. # # +v+:: The value to remove # # Returns true If the value exists and is removed # def remove_value(v) return !@values.delete(v).nil? end # Removes all values. # def clear_values @values = [] end # Returns an Array of the values of this Tag def values if block_given? @values.each { |v| yield v } else return @values end end # # Set the values for this tag. See {@link #addValue(Object)} for legal # value types. # # +values+:: The new values # @throws IllegalArgumentException if the collection contains any values # which are not legal SDL types # def values=(someValues) @values.clear() someValues.to_a.each { |v| # this is required to ensure validation of types add_value(v) } end # # Set an attribute in the given namespace for this tag. The allowable # attribute value types are the same as those allowed for # {@link #addValue(Object)} # # +namespace+:: The namespace for this attribute # +key+:: The attribute key # +value+:: The attribute value # @throws IllegalArgumentException if the key is not a legal SDL # identifier (see {@link SDL#validateIdentifier(String)}), or the # namespace is non-blank and is not a legal SDL identifier, or the # value is not a legal SDL type # def set_attribute(key, value, namespace = "") SDL4R.validate_identifier(namespace) unless namespace.empty? SDL4R.validate_identifier(key) attributes = @attributesByNamespace[namespace] if attributes.nil? attributes = {} @attributesByNamespace[namespace] = attributes end attributes[key] = SDL4R.coerce_or_fail(value) end # Returns the attribute of the specified +namespace+ of specified +key+ or +nil+ if not found. # # +namespace+:: the default namespace ("") by default # def attribute(key, namespace = "") attributes = @attributesByNamespace[namespace] return attributes.nil? ? nil : attributes[key] end # Indicates whether the specified attribute exists in this Tag. # # +key+:: key of the attribute # +namespace+:: namespace of the attribute ("", the default namespace, by default) # def has_attribute?(key, namespace = "") attributes = @attributesByNamespace[namespace] return attributes.nil? ? false : attributes.has_key?(key) end # Returns a copy of the Hash of the attributes of the specified +namespace+ (default is all). # # +namespace+:: namespace of the returned attributes. If nil, all attributes are returned with # qualified names (e.g. "meat:color"). If "" attributes of the default namespace are returned. # def attributes(namespace = nil, &block) if block_given? each_attribute(namespace, &block) else if namespace.nil? hash = {} each_attribute(nil) do | key, value, namespace | qualified_name = namespace.empty? ? key : namespace + ':' + key hash[qualified_name] = value end return hash else hash = @attributesByNamespace[namespace] return hash.clone end end end # Removes the attribute, whose name and namespace are specified. # # +key+:: name of the removed atribute # +namespace+:: namespace of the removed attribute (equal to "", default namespace, by default) # # Returns the value of the removed attribute or +nil+ if it didn't exist. # def remove_attribute(key, namespace = "") attributes = @attributesByNamespace[namespace] return attributes.nil? ? nil : attributes.delete(key) end # Clears the attributes of the specified namespace or all the attributes if +namespace+ is # +nil+. # def clear_attributes(namespace = nil) if namespace.nil? @attributesByNamespace.clear else @attributesByNamespace.delete(namespace) end end # Enumerates the attributes for the specified +namespace+. # If no +namespace+ is specified, enumerates the attribute of the default # namespace. If +namespace+ is nil, enumerates the attributes of all # namespaces. # def each_attribute(namespace = "", &block) if namespace.nil? @attributesByNamespace.each_key { |a_namespace| each_attribute(a_namespace, &block) } else attributes = @attributesByNamespace[namespace] unless attributes.nil? attributes.each_pair do |key, value| yield key, value, namespace end end end end private :each_attribute # Sets all the attributes of a +namespace+ for this Tag in one operation. # See # #add_value for allowable attribute value types. # # +attributes+:: a Hash where keys are attribute keys # +namespace+:: "" (default namespace) by default # # Raises an +ArgumentError+ if any key in the map is not a legal SDL # identifier (see SDL#validate_identifier), or any value # is not a legal SDL type. # def set_attributes(attribute_hash, namespace = "") return if attribute_hash.nil? or attribute_hash.empty? attributes = @attributesByNamespace[namespace] if attributes.nil? attributes = {} @attributesByNamespace[namespace] = attributes else attributes.clear() end attribute_hash.each_pair do |key, value| # Calling set_attribute() is required to ensure validations set_attribute(key, value) end end # Sets all the attributes of the default namespace for this Tag in one # operation. # # See #set_attributes # def attributes=(attribute_hash) set_attributes(attribute_hash, "") end # Sets the name of this Tag. # # Raises +ArgumentError+ if the name is not a legal SDL # identifier (see SDL#validate_identifier) def name=(a_name) a_name = a_name.to_s SDL4R.validate_identifier(a_name) @name = a_name end # The namespace to set. null will be coerced to the empty string. # # Raises +ArgumentError+ if the namespace is non-blank and is not # a legal SDL identifier (see {@link SDL#validate_identifier(String)}) def namespace=(a_namespace) a_namespace = a_namespace.to_s SDL4R.validate_identifier(a_namespace) unless a_namespace.empty? @namespace = a_namespace end # Adds all the tags specified in the given IO, String, Pathname or URI to this Tag. # # Returns this Tag after adding all the children read from +input+. # def read(input) if input.is_a? String read_from_io(true) { StringIO.new(input) } elsif input.is_a? Pathname read_from_io(true) { input.open("r") } elsif input.is_a? URI read_from_io(true) { input.open } else read_from_io(false) { input } end return self end # Reads and parses the +io+ returned by the specified block and closes this +io+ if +close_io+ # is true. def read_from_io(close_io) io = yield begin Parser.new(io).parse.each do |tag| add_child(tag) end ensure if close_io io.close rescue IOError end end end private_methods :read_io # Write this tag out to the given IO or string (optionally clipping the root.) # # +writer+:: The writer to which we will write this tag # +includeRoot+:: If true this tag will be written out as the root # element, if false only the children will be written # def write(output, include_root = false) io = (output.is_a?(String))? StringIO.new(output) : output if include_root io << to_s else each_child do |child, index| io << $/ if index > 0 child.write(io) end end io.close() end # # Get a String representation of this SDL Tag. This method returns a # complete description of the Tag's state using SDL (i.e. the output can # be parsed by {@link #read(String)}) # # Returns A string representation of this tag using SDL # def to_s to_string end # # +linePrefix+:: A prefix to insert before every line. # Returns A string representation of this tag using SDL # # TODO: break up long lines using the backslash # def to_string(line_prefix = "", indent = "\t") line_prefix = "" if line_prefix.nil? s = "" s << line_prefix if name == "content" && namespace.empty? skip_value_space = true else skip_value_space = false s << "#{namespace}:" unless namespace.empty? s << name end # output values values do |value| if skip_value_space skip_value_space = false else s << " " end s << SDL4R.format(value, true, line_prefix, indent) end # output attributes unless @attributesByNamespace.empty? all_attributes_hash = attributes(nil) all_attributes_array = all_attributes_hash.sort { |a, b| namespace1, name1 = a[0].split(':') namespace1, name1 = "", namespace1 if name1.nil? namespace2, name2 = b[0].split(':') namespace2, name2 = "", namespace2 if name2.nil? diff = namespace1 <=> namespace2 diff == 0 ? name1 <=> name2 : diff } all_attributes_array.each do |attribute_name, attribute_value| s << " " << attribute_name << '=' << SDL4R.format(attribute_value, true) end end # output children unless @children.empty? s << " {#{$/}" children_to_string(line_prefix + indent, s) s << line_prefix << ?} end return s end # Returns a string representation of the children tags. # # +linePrefix+:: A prefix to insert before every line. # +s+:: a String that receives the string representation # # TODO: break up long lines using the backslash # def children_to_string(line_prefix = "", s = "") @children.each do |child| s << child.to_string(line_prefix) << $/ end return s end # Returns true if this tag (including all of its values, attributes, and # children) is equivalent to the given tag. # # Returns true if the tags are equivalet # def eql?(o) # this is safe because to_string() dumps the full state return o.is_a?(Tag) && o.to_string == to_string; end alias_method :==, :eql? # Returns The hash (based on the output from toString()) # def hash return to_string.hash end # Returns a string containing an XML representation of this tag. Values # will be represented using _val0, _val1, etc. # # Returns An XML String describing this Tag # +linePrefix+:: A prefix to insert before every line. # Returns A String containing an XML representation of this tag. Values # will be represented using _val0, _val1, etc. # def to_xml_string(linePrefix = "") linePrefix = "" if linePrefix.nil? s = "" s << linePrefix << ?< s << "#{namespace}:" unless namespace.empty? s << name # output values unless @values.empty? i = 0 @values.each do |value| s << " _val" << i.to_s << "=\"" << SDL4R.format(value, false) << "\"" i += 1 end end # output attributes unless @attributes.empty? @attributes.each do |attribute_name, attribute_value| s << " " attribute_namespace = @attributeToNamespace[attribute_name] s << "#{attribute_namespace}:" unless attribute_namespace.empty? s << attribute_name << "=\"" << SDL4R.format(attribute_value, false) << ?" end end if @children.empty? s << "/>" else s << ">\n" @children.each do |child| s << child.to_xml_string(linePrefix + " ") << ?\n end s << linePrefix << " end return s end end end