+ module Kernel +
+ +unsuppress
+ +Public Class Methods
+# File lib/xml/rexml_ext.rb, line 155 +def warn(msg) +end+
diff --git a/.gitignore b/.gitignore deleted file mode 100644 index decca8f..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*~ -*.intout -*.lock -doc/api -user_manual.md -user_manual_xxpath.md -pkg/ \ No newline at end of file diff --git a/Kernel.html b/Kernel.html new file mode 100644 index 0000000..1655db0 --- /dev/null +++ b/Kernel.html @@ -0,0 +1,185 @@ + + + +
+ + +unsuppress
+ +# File lib/xml/rexml_ext.rb, line 155 +def warn(msg) +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 17 +def self.load_from_xml(xml, options={:mapping=>:_default}) + begin + Integer(xml.text) + rescue ArgumentError + Float(xml.text) + end +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 25 +def fill_into_xml(xml, options={:mapping=>:_default}) + xml.text = self.to_s +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 29 +def text + self.to_s +end+
Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.
+ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<item reference="RF-0001"> + <Description>Stuffed Penguin</Description> + <Quantity>10</Quantity> + <UnitPrice>8.95</UnitPrice> +</item>+ +
class Item + include XML::Mapping + + text_node :ref, "@reference" + text_node :descr, "Description" + numeric_node :quantity, "Quantity" + numeric_node :unit_price, "UnitPrice" + + def total_price + quantity*unit_price + end +end ++ +
i = Item.load_from_file("item.xml") +=> #<Item:0xb7888c90 @ref="RF-0001" @quantity=10, @descr="Stuffed Penguin", @unit_price=8.95> + +i.unit_price = 42.23 +xml=i.save_to_xml #convert to REXML node; there's also o.save_to_file(name) +xml.write($stdout,2) + +<item reference="RF-0001"> + <Description>Stuffed Penguin</Description> + <Quantity>10</Quantity> + <UnitPrice>42.23</UnitPrice> +</item>+ +
This is the most trivial example – the mapper supports arbitrary array and +hash (map) nodes, object (reference) nodes and arrays/hashes of those, +polymorphic mappings, multiple mappings per class, fully programmable +mappings and arbitrary user-defined node types. Read the project documentation for +more information.
+inline code snippets. TODO: switch to REXML::Formatters there sometime.
+ +# File lib/xml/rexml_ext.rb, line 171 +def write(output=$stdout, indent=-1, transitive=false, ie_hack=false) + Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters") + formatter = if indent > -1 + if transitive + require "rexml/formatters/transitive" + REXML::Formatters::Transitive.new( indent, ie_hack ) + else + REXML::Formatters::Pretty.new( indent, ie_hack ) + end + else + REXML::Formatters::Default.new( ie_hack ) + end + formatter.write( self, output ) +end+
# File lib/xml/rexml_ext.rb, line 142 +def each_on_axis(axis, &block) + send :"each_on_axis_#{axis}", &block +end+
# File lib/xml/rexml_ext.rb, line 118 +def each_on_axis_child + if respond_to? :attributes + attributes.each_key do |name| + yield XML::XXPath::Accessors::Attribute.new(self, name, false) + end + end + each_child do |c| + yield c + end +end+
# File lib/xml/rexml_ext.rb, line 129 +def each_on_axis_descendant(&block) + each_on_axis_child do |c| + block.call c + if REXML::Parent===c + c.each_on_axis_descendant(&block) + end + end +end+
# File lib/xml/rexml_ext.rb, line 138 +def each_on_axis_self + yield self +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 2 +def self.load_from_xml(xml, options={:mapping=>:_default}) + xml.text +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 6 +def fill_into_xml(xml, options={:mapping=>:_default}) + xml.text = self +end+
# File lib/xml/mapping/core_classes_mapping.rb, line 10 +def text + self +end+
consider switching from REXML to nokogiri and/or, +maybe, ox.
+XML::XXPath: Write a real XPath parser eventually
+XML::XXPath: avoid duplicates in path.all(node) result arrays when using +the descendants (“//”) axis
+invent an XPath-like language for Ruby object graphs (i.e. a language that +is to Ruby object graphs what XPath is to XML +trees). Use expressions in that language as a generalization of “attribute +names” (e.g. the 1st parameter to single attribute node factory methods). +The language could more or less be Ruby itself, but the write support would +need some extra work…
+XML::XXPath:
+implement .[@attrname] steps
+returns the context node iff it contains an attrname attribute
+doesn't work properly in REXML::XPath?
+implement *[@attrname] steps
+implement *[@attrname='attrvalue'] steps
+id/idref support (write support possible?)
+XML::Mapping: make SubObjectBaseNode a mixin instead of a subclass of +SingleAttributeNode (“mapping sub-objects” and “mapping to a single +attribute” are orthogonal concepts; inheritance is bad design here)
+documentation:
+consider switching to YARD
+reasons: parameter/return type metadata, (maybe) plugin for the code +snippet inclusion stuff
+document/show usage of default_when_xpath_err outside node type +implementations
+mention new step types, new axes, xml/xpath_methods
+XML::XXPath/XML::Mapping: support for XML namespaces +in XML::XXPath (match nodes with specific +namespaces only) and XML::Mapping +(use_namespace etc.)
+add streaming input/output to XML::Mapping, +i.e. SAX-based input in addition to the current REXML/DOM - based one. +Probably won't be implementable for some more complicated XPaths – +raise meaningful exceptions in those cases.
+would need support in xxpath
+should probably be built on top of the Ruby 2.0 lazy enumeration stuff
+XML::XXPath/XML::Mapping: add XML text nodes (the +sub-node of an element node that contains that element's text) +first-class to XML::XXPath. Use it for things +like text_node :contents, “text()”.
+ +Along those lines: promote XPath node “unspecifiedness” from an attribute +to a REXML node object of “unspecified” class +that's turned into an attribute/element/text node when necessary
+(eventually, maybe) provide a “scaffolding” feature to automatically turn a +dtd/schema into a set of node type definitions or even a set of mapping +classes
+xml-mapping – bidirectional Ruby-XML mapper
+ +Copyright (C) 2004,2005 Olaf Klischat+ +
xml-mapping – bidirectional Ruby-XML mapper
+ +Copyright (C) 2004-2010 Olaf Klischat+ +
xxpath – XPath implementation for Ruby, including write access
+ +Copyright (C) 2004-2006 Olaf Klischat+ +
This is the central interface module of the xml-mapping library.
+ +Including this module in your classes adds XML +mapping capabilities to them.
+ +<company name="ACME inc."> + + <address> + <city>Berlin</city> + <zip>10113</zip> + </address> + + <customers> + + <customer id="jim"> + <name>James Kirk</name> + </customer> + + <customer id="ernie"> + <name>Ernie</name> + </customer> + + <customer id="bert"> + <name>Bert</name> + </customer> + + </customers> + +</company>+ +
require 'xml/mapping' + +## forward declarations +class Address; end +class Customer; end + +class Company + include XML::Mapping + + text_node :name, "@name" + object_node :address, "address", :class=>Address + array_node :customers, "customers", "customer", :class=>Customer +end + +class Address + include XML::Mapping + + text_node :city, "city" + numeric_node :zip, "zip" +end + +class Customer + include XML::Mapping + + text_node :id, "@id" + text_node :name, "name" + + def initialize(id,name) + @id,@name = [id,name] + end +end ++ +
c = Company.load_from_file('company.xml') +=> #<Company:0x007ff64a1077a8 @name="ACME inc.", @address=#<Address:0x007ff64a106f60 @city="Berlin", @zip=10113>, @customers=[#<Customer:0x007ff64a105700 @id="jim", @name="James Kirk">, #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie">, #<Customer:0x007ff64a0ff030 @id="bert", @name="Bert">]> +c.name +=> "ACME inc." +c.customers.size +=> 3 +c.customers[1] +=> #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie"> +c.customers[1].name +=> "Ernie" +c.customers[0].name +=> "James Kirk" +c.customers[0].name = 'James Tiberius Kirk' +=> "James Tiberius Kirk" +c.customers << Customer.new('cm','Cookie Monster') +=> [#<Customer:0x007ff64a105700 @id="jim", @name="James Tiberius Kirk">, #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie">, #<Customer:0x007ff64a0ff030 @id="bert", @name="Bert">, #<Customer:0x007ff64a0f78f8 @id="cm", @name="Cookie Monster">] +xml2 = c.save_to_xml +=> <company name='ACME inc.'> ... </> +xml2.write($stdout,2) +<company name='ACME inc.'> + <address> + <city> + Berlin + </city> + <zip> + 10113 + </zip> + </address> + <customers> + <customer id='jim'> + <name> + James Tiberius Kirk + </name> + </customer> + <customer id='ernie'> + <name> + Ernie + </name> + </customer> + <customer id='bert'> + <name> + Bert + </name> + </customer> + <customer id='cm'> + <name> + Cookie Monster + </name> + </customer> + </customers> +</company>#+ +
So you have to include XML::Mapping into your +class to turn it into a “mapping class”, that is, to add XML mapping capabilities to it. An instance of the +mapping classes is then bidirectionally mapped to an XML node (i.e. an element), where the state (simple +attributes, sub-objects, arrays, hashes etc.) of that instance is mapped to +sub-nodes of that node. In addition to the class and instance methods +defined in XML::Mapping, your mapping class will +get class methods like 'text_node', 'array_node' and so on; +I call them “node factory methods”. More precisely, there is one node +factory method for each registered node type. Node types are classes derived from XML::Mapping::Node; they're registered +with the xml-mapping library via ::add_node_class. The node +types TextNode, BooleanNode, NumericNode, ObjectNode, ArrayNode, and HashNode are automatically registered by +xml/mapping.rb; you can easily write your own ones. The name of a node +factory method is inferred by 'underscoring' the name of the +corresponding node type; e.g. 'TextNode' becomes +'text_node'. Each node factory method creates an instance of the +corresponding node type and adds it to the mapping class (not its +instances). The arguments to a node factory method are automatically turned +into arguments to the corresponding node type's initializer. So, in +order to learn more about the meaning of a node factory method's +parameters, you read the documentation of the corresponding node type. All +predefined node types expect as their first argument a symbol that names an +r/w attribute which will be added to the mapping class. The mapping class +is a normal Ruby class; you can add constructors, methods and attributes to +it, derive from it, derive it from another class, include additional +modules etc.
+ +Including XML::Mapping also adds all methods of +XML::Mapping::ClassMethods to your +class (as class methods).
+ +It is recommended that if your class does not have required
+initialize
method arguments. The XML
+loader attempts to create a new object using the new
method.
+If this fails because the initializer expects an argument, then the loader
+calls allocate
instead. allocate
bypasses the
+initializer. If your class must have initializer arguments, then you
+should verify that bypassing the initializer is acceptable.
As you may have noticed from the example, the node factory methods +generally use XPath expressions to specify locations in the mapped XML document. To make this work, XML::Mapping relies on XML::XXPath, which implements a subset of XPath, but +also provides write access, which is needed by the node types to support +writing data back to XML. Both XML::Mapping and XML::XXPath use REXML +(www.germane-software.com/software/rexml/) +to represent XML elements/documents in memory.
+ +Registers the new node class c (must be a descendant of Node) with the xml-mapping framework.
+ +A new “factory method” will automatically be added to ClassMethods (and therefore to all
+classes that include XML::Mapping from now on);
+so you can call it from the body of your mapping class definition in order
+to create nodes of type c. The name of the factory method is
+derived by “underscoring” the (unqualified) name of c; e.g.
+c==Foo::Bar::MyNiftyNode
will result in the creation
+of a factory method named my_nifty_node
. The generated factory
+method creates and returns a new instance of c. The list of
+argument to c.new consists of self (i.e. the mapping
+class the factory method was called from) followed by the arguments passed
+to the factory method. You should always use the factory methods to create
+instances of node classes; you should never need to call a node class's
+constructor directly.
For a demonstration, see the calls to text_node
,
+array_node
etc. in the examples along with the corresponding
+node classes TextNode, ArrayNode etc. (these predefined node
+classes are in no way “special”; they're added using ::add_node_class in
+mapping.rb just like any custom node classes would be).
# File lib/xml/mapping/base.rb, line 505 +def self.add_node_class(c) + meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase} + ClassMethods.module_eval <<-EOS + def #{meth_name}(*args) + #{c.name}.new(self,*args) + end + EOS +end+
Finds a mapping class and mapping name corresponding to the given XML root element name. There may be more than one +(class,mapping) tuple for a given root element name – in that case, one of +them is selected arbitrarily.
+ +returns [class,mapping]
+ + + + +# File lib/xml/mapping/base.rb, line 134 +def self.class_and_mapping_for_root_elt_name(name) + (Classes_by_rootelt_names[name] || {}).each_pair{|mapping,classes| return [classes[0],mapping] } + nil +end+
Finds a mapping class corresponding to the given XML root element name and mapping name. There may be +more than one such class – in that case, the most recently defined one is +returned
+ +This is the inverse operation to <class>.root_element_name (see XML::Mapping::ClassMethods#root_element_name).
+ + + + +# File lib/xml/mapping/base.rb, line 122 +def self.class_for_root_elt_name(name, options={:mapping=>:_default}) + # TODO: implement Hash read-only instead of this + # interface + Classes_by_rootelt_names.classes_for(name, options[:mapping])[-1] +end+
Like ::load_object_from_xml, +but loads from the XML file named by +filename.
+ + + + +# File lib/xml/mapping/base.rb, line 475 +def self.load_object_from_file(filename,options={:mapping=>nil}) + xml = REXML::Document.new(File.new(filename)) + load_object_from_xml xml.root, options +end+
“polymorphic” load function. Turns the XML tree +xml into an object, which is returned. The class of the object and +the mapping to be used for unmarshalling are automatically determined from +the root element name of xml using ::class_for_root_elt_name. +If :mapping is non-nil, only root element names defined in that mapping +will be considered (default is to consider all classes)
+ + + + +# File lib/xml/mapping/base.rb, line 461 +def self.load_object_from_xml(xml,options={:mapping=>nil}) + if mapping = options[:mapping] + c = class_for_root_elt_name xml.name, :mapping=>mapping + else + c,mapping = class_and_mapping_for_root_elt_name(xml.name) + end + unless c + raise MappingError, "no mapping class for root element name #{xml.name}, mapping #{mapping.inspect}" + end + c.load_from_xml xml, :mapping=>mapping +end+
Initializer. Called (by Class#new) after self was created using +new.
+ +XML::Mapping's implementation calls initialize_xml_mapping.
+ + +# File lib/xml/mapping/base.rb, line 169 +def initialize(*args) + super(*args) + initialize_xml_mapping +end+
“fill” the contents of xml into self. xml is a +REXML::Element.
+ +First, #pre_load(xml) +is called, then all the nodes for this object's class are processed +(i.e. have their xml_to_obj method called) in the order of their definition +inside the class, then post_load is called.
+ + + + +# File lib/xml/mapping/base.rb, line 181 +def fill_from_xml(xml, options={:mapping=>:_default}) + raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") unless self.class.xml_mapping_nodes_hash.has_key?(options[:mapping]) + pre_load xml, :mapping=>options[:mapping] + self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| + node.xml_to_obj self, xml + end + post_load :mapping=>options[:mapping] +end+
Fill self's state into the xml node (REXML::Element) +xml. All the nodes for this object's class are processed (i.e. +have their obj_to_xml method called) in the order of their definition +inside the class.
+ + + + +# File lib/xml/mapping/base.rb, line 216 +def fill_into_xml(xml, options={:mapping=>:_default}) + self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| + node.obj_to_xml self,xml + end +end+
Xml-mapping-specific initializer.
+ +This will be called when a new instance is being initialized from an XML source, as well as after calling +class.new(args) (for the latter case to work, you'll +have to make sure you call the inherited initialize method)
+ +The :mapping keyword argument gives the mapping the instance is being +initialized with. This is non-nil only when the instance is being +initialized from an XML source (:mapping will +contain the :mapping argument passed (explicitly or implicitly) to the +load_from_… method).
+ +When the instance is being initialized because class.new +was called, the :mapping argument is set to nil to show that the object is +being initialized with respect to no specific mapping.
+ +The default implementation of this method calls obj_initializing on all
+nodes. You may overwrite this method to do your own initialization stuff;
+make sure to call super
in that case.
# File lib/xml/mapping/base.rb, line 159 +def initialize_xml_mapping(options={:mapping=>nil}) + self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| + node.obj_initializing(self,options[:mapping]) + end +end+
This method is called immediately after self has been filled from +an xml source. If you have things to do after the object has been +succefully loaded from the xml (reorganising the loaded data in some way, +setting up additional views on the data etc.), this is the place where you +put them. You can also raise an exception to abandon the whole loading +process.
+ +The default implementation of this method is empty.
+ + + + +# File lib/xml/mapping/base.rb, line 207 +def post_load(options={:mapping=>:_default}) +end+
This method is called immediately after self's state has been +filled into an XML element.
+ +The default implementation does nothing.
+ + + + +# File lib/xml/mapping/base.rb, line 253 +def post_save(xml, options={:mapping=>:_default}) +end+
This method is called immediately before self is filled from an +xml source. xml is the source REXML::Element.
+ +The default implementation of this method is empty.
+ + + + +# File lib/xml/mapping/base.rb, line 195 +def pre_load(xml, options={:mapping=>:_default}) +end+
This method is called when self is to be converted to an XML tree. It must create and return +an XML element (as a REXML::Element); that element will then be +passed to fill_into_xml.
+ +The default implementation of this method creates a new empty element whose +name is the root_element_name of self's class (see XML::Mapping::ClassMethods#root_element_name). +By default, this is the class name, with capital letters converted to +lowercase and preceded by a dash, e.g. “MySampleClass” becomes +“my-sample-class”.
+ + + + +# File lib/xml/mapping/base.rb, line 245 +def pre_save(options={:mapping=>:_default}) + REXML::Element.new(self.class.root_element_name(:mapping=>options[:mapping])) +end+
Save self's state as XML into the +file named filename. The XML is obtained +by calling save_to_xml.
+ + + + +# File lib/xml/mapping/base.rb, line 259 +def save_to_file(filename, options={:mapping=>:_default}) + xml = save_to_xml :mapping=>options[:mapping] + formatter = options[:formatter] || self.class.mapping_output_formatter + File.open(filename,"w") do |f| + formatter.write(xml, f) + end +end+
Fill self's state into a new xml node, return that node.
+ +This method calls pre_save, +then fill_into_xml, then +post_save.
+ + + + +# File lib/xml/mapping/base.rb, line 227 +def save_to_xml(options={:mapping=>:_default}) + xml = pre_save :mapping=>options[:mapping] + fill_into_xml xml, :mapping=>options[:mapping] + post_save xml, :mapping=>options[:mapping] + xml +end+
Node factory function synopsis:
+ +array_node :_attrname_, _per_arrelement_path_ + [, :default_value=>_obj_] + [, :optional=>true] + [, :class=>_c_] + [, :marshaller=>_proc_] + [, :unmarshaller=>_proc_] + [, :mapping=>_m_] + [, :sub_mapping=>_sm_]+ +
-or-
+ +array_node :_attrname_, _base_path_, _per_arrelement_path_ + [keyword args the same]+ +
Node that maps a sequence of sub-nodes of the XML tree to an attribute containing an array of +Ruby objects, with each array element mapping to a corresponding member of +the sequence of sub-nodes.
+ +If base_path is not supplied, it is assumed to be “”.
+base_path+"/"
+per_arrelement_path
+is an XPath expression that must “yield” the sequence of XML nodes that is to be mapped to the array. The
+difference between base_path and per_arrelement_path
+becomes important when marshalling the array attribute back to XML. When that happens, base_path names
+the most specific common parent node of all the mapped sub-nodes, and
+per_arrelement_path names (relative to base_path) the
+part of the path that is duplicated for each array element. For example,
+with base_path=="foo/bar"
and
+per_arrelement_path=="hi/ho"
, an array
+[x,y,z]
will be written to an XML
+structure that looks like this:
<foo> + <bar> + <hi> + <ho> + [marshalled object x] + </ho> + </hi> + <hi> + <ho> + [marshalled object y] + </ho> + </hi> + <hi> + <ho> + [marshalled object z] + </ho> + </hi> + </bar> +</foo>+ +
Initializer. Called with keyword arguments and either 1 or 2 paths; the +hindmost path argument passed is delegated to per_arrelement_path; +the preceding path argument (if present, “” by default) is delegated to +base_path.
+ + +# File lib/xml/mapping/standard_nodes.rb, line 270 +def initialize(*args) + path,path2,*args = super(*args) + base_path,per_arrelement_path = if path2 + [path,path2] + else + [".",path] + end + per_arrelement_path=per_arrelement_path[1..-1] if per_arrelement_path[0]==?/ + @base_path = XML::XXPath.new(base_path) + @per_arrelement_path = XML::XXPath.new(per_arrelement_path) + @reader_path = XML::XXPath.new(base_path+"/"+per_arrelement_path) + args +end+
Node factory function synopsis:
+ +boolean_node :_attrname_, _path_, + _true_value_, _false_value_ [, :default_value=>_obj_] + [, :optional=>true] + [, :mapping=>_m_]+ +
Node that maps an XML
+node's text (the element name resp. the attribute value) to a boolean
+attribute of the mapped object. The attribute named by :attrname
+is mapped to/from the XML subnode named by the
+XPath expression path. true_value is the text the node
+must have in order to represent the true
boolean value,
+false_value (actually, any value other than true_value)
+is the text the node must have in order to represent the false
+boolean value.
Initializer.
+ + +# File lib/xml/mapping/standard_nodes.rb, line 197 +def initialize(*args) + path,true_value,false_value,*args = super(*args) + @path = XML::XXPath.new(path) + @true_value = true_value; @false_value = false_value + args +end+
# File lib/xml/mapping/standard_nodes.rb, line 371 +def initialize(*args) + args = super(*args) + @choices = [] + path=nil + args.each do |arg| + next if [:if,:then,:elsif].include? arg + if path.nil? + path = (if [:else,:default,:otherwise].include? arg + :else + else + XML::XXPath.new arg + end) + else + raise XML::MappingError, "node expected, found: #{arg.inspect}" unless Node===arg + @choices << [path,arg] + + # undo what the node factory fcn did -- ugly ugly! would + # need some way to lazy-evaluate arg (a proc would be + # simple but ugly for the user), and then use some + # mechanism (a flag with dynamic scope probably) to tell + # the node factory fcn not to add the node to the + # xml_mapping_nodes + @owner.xml_mapping_nodes(:mapping=>@mapping).delete arg + path=nil + end + end + + raise XML::MappingError, "node missing at end of argument list" unless path.nil? + raise XML::MappingError, "no choices were supplied" if @choices.empty? + + [] +end+
(overridden) true if at least one of our nodes is_present_in? obj.
+ + + + +# File lib/xml/mapping/standard_nodes.rb, line 432 +def is_present_in? obj + # TODO: use Enumerable#any? + @choices.inject(false){|prev,(path,node)| prev or node.is_present_in?(obj)} +end+
# File lib/xml/mapping/standard_nodes.rb, line 426 +def obj_initializing(obj,mapping) + @choices[0][1].obj_initializing(obj,mapping) +end+
# File lib/xml/mapping/standard_nodes.rb, line 414 +def obj_to_xml(obj,xml) + @choices.each do |path,node| + if node.is_present_in? obj + node.obj_to_xml(obj,xml) + path.first(xml, :ensure_created=>true) + return true + end + end + # @choices[0][1].obj_to_xml(obj,xml) + raise XML::MappingError, "obj_to_xml: no choice present in object: #{obj.inspect}" +end+
# File lib/xml/mapping/standard_nodes.rb, line 404 +def xml_to_obj(obj,xml) + @choices.each do |path,node| + if path==:else or not(path.all(xml).empty?) + node.xml_to_obj(obj,xml) + return true + end + end + raise XML::MappingError, "xml_to_obj: no choice matched in: #{xml}" +end+
The instance methods of this module are automatically added as class +methods to a class that includes XML::Mapping.
+ +Add getter and setter methods for a new attribute named name (must +be a symbol or a string) to this class, taking care not to replace existing +getters/setters. This is a convenience method intended to be called from +Node class initializers.
+ + + + +# File lib/xml/mapping/base.rb, line 308 +def add_accessor(name) + # existing methods search. Search for symbols and strings + # to be compatible with Ruby 1.8 and 1.9. + methods = self.instance_methods + if methods[0].kind_of? Symbol + getter = :"#{name}" + setter = :"#{name}=" + else + getter = "#{name}" + setter = "#{name}=" + end + unless methods.include?(getter) + self.module_eval <<-EOS + attr_reader :#{name} + EOS + end + unless methods.include?(setter) + self.module_eval <<-EOS + attr_writer :#{name} + EOS + end +end+
enumeration of all nodes in effect when marshalling/unmarshalling this +class, that is, nodes defined for this class as well as for its +superclasses. The nodes are returned in the order of their definition, +starting with the topmost superclass that has nodes defined. keyword +arguments are the same as for xml_mapping_nodes.
+ + + + +# File lib/xml/mapping/base.rb, line 394 +def all_xml_mapping_nodes(options={:mapping=>nil,:create=>true}) + # TODO: we could return a dynamic Enumerable here, or cache + # the array... + result = [] + if superclass and superclass.respond_to?(:all_xml_mapping_nodes) + result += superclass.all_xml_mapping_nodes options + end + result += xml_mapping_nodes options +end+
return the current default mapping (:_default initially, or the value set +with the latest call to #use_mapping)
+ + + + +# File lib/xml/mapping/base.rb, line 300 +def default_mapping + @default_mapping +end+
The default root element name for this class. Equals the class name, with +all parent module names stripped, and with capital letters converted to +lowercase and preceded by a dash; e.g. “Foo::Bar::MySampleClass” becomes +“my-sample-class”.
+ + + + +# File lib/xml/mapping/base.rb, line 435 +def default_root_element_name + self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase} +end+
Create a new instance of this class from the XML contained in the file named +filename. Calls #load_from_xml +internally.
+ + + + +# File lib/xml/mapping/base.rb, line 333 +def load_from_file(filename, options={:mapping=>:_default}) + xml = REXML::Document.new(File.new(filename)) + load_from_xml xml.root, :mapping=>options[:mapping] +end+
Create a new instance of this class from the XML contained in xml (a REXML::Element).
+ +Allocates a new object, then calls fill_from_xml(xml) on it.
+ + + + +# File lib/xml/mapping/base.rb, line 343 +def load_from_xml(xml, options={:mapping=>:_default}) + raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") unless xml_mapping_nodes_hash.has_key?(options[:mapping]) + # create the new object. It is recommended that the class + # have a no-argument initializer, so try new first. If that + # doesn't work, try allocate, which bypasses the initializer. + begin + obj = self.new + #TODO: this will normally invoke our base XML::Mapping#initialize, which calls + # obj.initialize_xml_mapping, which is called below again (with the correct :mapping parameter). + # obj.initialize_xml_mapping calls obj_initializing on all nodes. + # So obj_initializing may be called on the nodes twice for this initialization. + # Maybe document this for node writers? + rescue ArgumentError # TODO: this may hide real errors. + # how to statically check whether + # self self.new accepts an empty + # argument list? + obj = self.allocate + end + obj.initialize_xml_mapping :mapping=>options[:mapping] + obj.fill_from_xml xml, :mapping=>options[:mapping] + obj +end+
the formatter to be used for output formatting when writing xml to +character streams (Files/IOs). Combined getter/setter. Defaults to simple +(compact/no-whitespace) formatting, may be overridden on a per-call base +via options
+ + + + +# File lib/xml/mapping/base.rb, line 443 +def mapping_output_formatter(formatter=nil) + # TODO make it per-mapping + if formatter + @mapping_output_formatter = formatter + end + @mapping_output_formatter ||= REXML::Formatters::Default.new +end+
The “root element name” of this class (combined getter/setter method).
+ +The root element name is the name of the root element of the XML tree returned by <this +class>.#save_to_xml (or, more specifically, <this +class>.#pre_save). By default, this method returns the default_root_element_name; +you may call this method with an argument to set the root element name to +something other than the default. The option argument :mapping specifies +the mapping the root element is/will be defined in, it defaults to the +current default mapping (:_default initially, or the value set with the +latest call to #use_mapping)
+ + + + +# File lib/xml/mapping/base.rb, line 418 +def root_element_name(name=nil, options={:mapping=>@default_mapping}) + if Hash===name # ugly... + options=name; name=nil + end + @root_element_names ||= {} + if name + Classes_by_rootelt_names.remove_class root_element_name, options[:mapping], self + @root_element_names[options[:mapping]] = name + Classes_by_rootelt_names.create_classes_for(name, options[:mapping]) << self + end + @root_element_names[options[:mapping]] || default_root_element_name +end+
Make mapping the mapping to be used by default in future node +declarations in this class. The default can be overwritten on a per-node +basis by passing a :mapping option parameter to the node factory method
+ +The initial default mapping in a mapping class is :_default
+ + + + +# File lib/xml/mapping/base.rb, line 291 +def use_mapping mapping + @default_mapping = mapping + xml_mapping_nodes_hash[mapping] ||= [] # create empty mapping node list if + # there wasn't one before so future calls + # to load/save_xml etc. w/ this mapping don't raise +end+
array of all nodes defined in this class, in the order of their definition. +Option :create specifies whether or not an empty array should be created +and returned if there was none before (if not, an exception is raised). +:mapping specifies the mapping the returned nodes must have been defined +in; nil means return all nodes regardless of their mapping
+ + + + +# File lib/xml/mapping/base.rb, line 374 +def xml_mapping_nodes(options={:mapping=>nil,:create=>true}) + unless options[:mapping] + return xml_mapping_nodes_hash.values.inject([]){|a1,a2|a1+a2} + end + options[:create] = true if options[:create].nil? + if options[:create] + xml_mapping_nodes_hash[options[:mapping]] ||= [] + else + xml_mapping_nodes_hash[options[:mapping]] || + raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") + end +end+
# File lib/xml/mapping/base.rb, line 95 +def classes_for rootelt_name, mapping + (self[rootelt_name] || {})[mapping] || [] +end+
# File lib/xml/mapping/base.rb, line 92 +def create_classes_for rootelt_name, mapping + (self[rootelt_name] ||= {})[mapping] ||= [] +end+
# File lib/xml/mapping/base.rb, line 101 +def ensure_exists rootelt_name, mapping, clazz + clazzes = create_classes_for(rootelt_name, mapping) + clazzes << clazz unless clazzes.include? clazz +end+
# File lib/xml/mapping/base.rb, line 98 +def remove_class rootelt_name, mapping, clazz + classes_for(rootelt_name, mapping).delete clazz +end+
Node factory function synopsis:
+ +hash_node :_attrname_, _per_hashelement_path_, _key_path_ + [, :default_value=>_obj_] + [, :optional=>true] + [, :class=>_c_] + [, :marshaller=>_proc_] + [, :unmarshaller=>_proc_] + [, :mapping=>_m_] + [, :sub_mapping=>_sm_]+
or -
+ +hash_node :attrname, base_path, +per_hashelement_path, key_path
+ +[keyword args the same]+
Node that maps a sequence of sub-nodes of the XML tree to an attribute containing a hash of +Ruby objects, with each hash value mapping to a corresponding member of the +sequence of sub-nodes. The (string-valued) hash key associated with a hash +value v is mapped to the text of a specific sub-node of +v's sub-node.
+ +Analogously to ArrayNode, base_path +and per_arrelement_path define the XPath expression that “yields” +the sequence of XML nodes, each of which maps +to a value in the hash table. Relative to such a node, key_path_ names the +node whose text becomes the associated hash key.
+ +Initializer. Called with keyword arguments and either 2 or 3 paths; the +hindmost path argument passed is delegated to key_path, the +preceding path argument is delegated to per_arrelement_path, the +path preceding that argument (if present, “” by default) is delegated to +base_path. The meaning of the keyword arguments is the same as for +ObjectNode.
+ + +# File lib/xml/mapping/standard_nodes.rb, line 335 +def initialize(*args) + path1,path2,path3,*args = super(*args) + base_path,per_hashelement_path,key_path = if path3 + [path1,path2,path3] + else + ["",path1,path2] + end + per_hashelement_path=per_hashelement_path[1..-1] if per_hashelement_path[0]==?/ + @base_path = XML::XXPath.new(base_path) + @per_hashelement_path = XML::XXPath.new(per_hashelement_path) + @key_path = XML::XXPath.new(key_path) + @reader_path = XML::XXPath.new(base_path+"/"+per_hashelement_path) + args +end+
Abstract base class for all node types. As mentioned in the documentation +for XML::Mapping, node types must be +registered using XML::Mapping.add_node_class, +and a corresponding “node factory method” (e.g. “text_node”) will then be +added as a class method to your mapping classes. The node factory method is +called from the body of the mapping classes as demonstrated in the +examples. It creates an instance of its corresponding node type (the list +of parameters to the node factory method, preceded by the owning mapping +class, will be passed to the constructor of the node type) and adds it to +its owning mapping class, so there is one node object per node definition +per mapping class. That node object will handle all XML marshalling/unmarshalling for this node, for +all instances of the mapping class. For this purpose, the marshalling and +unmarshalling methods of a mapping class instance (fill_into_xml and +fill_from_xml, respectively) will call ::obj_to_xml resp. ::xml_to_obj on all nodes of the +mapping class, in the order of their definition, passing the REXML element the data is to be marshalled +to/unmarshalled from as well as the object the data is to be read +from/filled into.
+ +Node types that map some XML data to a single attribute of their mapping +class (that should be most of them) shouldn't be directly derived from +this class, but rather from SingleAttributeNode.
+ +Intializer, to be called from descendant classes. owner is the +mapping class this node is being defined in. It'll be stored in +_@owner_. @options will be set to a (possibly empty) hash containing the +option arguments passed to initialize. Options :mapping, :reader +and :writer will be handled, subclasses may handle additional options. See +the section on defining nodes in the README for details.
+ + + + +# File lib/xml/mapping/base.rb, line 550 +def initialize(owner,*args) + @owner = owner + if Hash===args[-1] + @options = args[-1] + args = args[0..-2] + else + @options={} + end + @mapping = @options[:mapping] || owner.default_mapping + owner.xml_mapping_nodes(:mapping=>@mapping) << self + XML::Mapping::Classes_by_rootelt_names.ensure_exists owner.root_element_name, @mapping, owner + if @options[:reader] + # override xml_to_obj in this instance with invocation of + # @options[:reader] + class << self + alias_method :default_xml_to_obj, :xml_to_obj + def xml_to_obj(obj,xml) + begin + @options[:reader].call(obj,xml,self.method(:default_xml_to_obj)) + rescue ArgumentError # thrown if @options[:reader] is a lambda (i.e. no Proc) with !=3 args (e.g. proc{...} in ruby1.8) + @options[:reader].call(obj,xml) + end + end + end + end + if @options[:writer] + # override obj_to_xml in this instance with invocation of + # @options[:writer] + class << self + alias_method :default_obj_to_xml, :obj_to_xml + def obj_to_xml(obj,xml) + begin + @options[:writer].call(obj,xml,self.method(:default_obj_to_xml)) + rescue ArgumentError # thrown if (see above) + @options[:writer].call(obj,xml) + end + end + end + end + args +end+
# File lib/xml/mapping/base.rb, line 580 +def obj_to_xml(obj,xml) + begin + @options[:writer].call(obj,xml,self.method(:default_obj_to_xml)) + rescue ArgumentError # thrown if (see above) + @options[:writer].call(obj,xml) + end +end+
# File lib/xml/mapping/base.rb, line 566 +def xml_to_obj(obj,xml) + begin + @options[:reader].call(obj,xml,self.method(:default_xml_to_obj)) + rescue ArgumentError # thrown if @options[:reader] is a lambda (i.e. no Proc) with !=3 args (e.g. proc{...} in ruby1.8) + @options[:reader].call(obj,xml) + end +end+
tell whether this node's data is present in obj (when this +method is called, obj will be an instance of the mapping class +this node was defined in). This method is currently used only by ChoiceNode when writing data back to XML. See XML::Mapping::ChoiceNode#obj_to_xml.
+ + + + +# File lib/xml/mapping/base.rb, line 629 +def is_present_in? obj + true +end+
Called when a new instance of the mapping class this node belongs to is
+being initialized. obj is the instance. mapping is the
+mapping the initialization is happening with, if any: If the instance is
+being initialized as part of e.g. Class.load_from_file(name,
+:mapping=>:some_mapping
or any other call that specifies a
+mapping, that mapping will be passed to this method. If the instance is
+being initialized normally with Class.new
, mapping is
+nil here.
You may set up initial values for the attributes this node is responsible +for here. Default implementation is empty.
+ + + + +# File lib/xml/mapping/base.rb, line 622 +def obj_initializing(obj,mapping) +end+
This is called by the XML unmarshalling +machinery when the state of an instance of this node's @owner is to be +stored into an XML tree. obj is the +instance, xml is the tree (a REXML::Element). The node must extract +“its” data from obj and store it to the corresponding parts +(sub-elements, attributes etc.) of xml (using XML::XXPath or any other means).
+ + + + +# File lib/xml/mapping/base.rb, line 607 +def obj_to_xml(obj,xml) + raise "abstract method called" +end+
This is called by the XML unmarshalling +machinery when the state of an instance of this node's @owner is to be +read from an XML tree. obj is the +instance, xml is the tree (a REXML::Element). The node must read +“its” data from xml (using XML::XXPath or any other means) and store it to +the corresponding parts (attributes etc.) of obj's state.
+ + + + +# File lib/xml/mapping/base.rb, line 597 +def xml_to_obj(obj,xml) + raise "abstract method called" +end+
Node factory function synopsis:
+ +numeric_node :_attrname_, _path_ [, :default_value=>_obj_] + [, :optional=>true] + [, :mapping=>_m_]+ +
Like TextNode, but interprets the XML node's text as a number (Integer or +Float, depending on the nodes's text) and maps it to an Integer or +Float attribute.
+ +# File lib/xml/mapping/standard_nodes.rb, line 46 +def initialize(*args) + path,*args = super(*args) + @path = XML::XXPath.new(path) + args +end+
Node factory function synopsis:
+ +object_node :_attrname_, _path_ [, :default_value=>_obj_] + [, :optional=>true] + [, :class=>_c_] + [, :marshaller=>_proc_] + [, :unmarshaller=>_proc_] + [, :mapping=>_m_] + [, :sub_mapping=>_sm_]+ +
Node that maps a subtree in the source XML to a Ruby object. :attrname and
+path are again the attribute name resp. XPath expression of the
+mapped attribute; the keyword arguments :default_value
and
+:optional
are handled by the SingleAttributeNode superclass. The XML subnode named by path is mapped to
+the attribute named by :attrname according to the keyword
+arguments :class
, :marshaller
, and
+:unmarshaller
, which are handled by the SubObjectBaseNode superclass.
Initializer. path (a string denoting an XPath expression) is the +location of the subtree.
+ + +# File lib/xml/mapping/standard_nodes.rb, line 167 +def initialize(*args) + path,*args = super(*args) + @path = XML::XXPath.new(path) + args +end+
Base class for node types that map some XML +data to a single attribute of their mapping class.
+ +All node types that come with xml-mapping except one (ChoiceNode) inherit +from SingleAttributeNode.
+ +Initializer. owner is the owning mapping class (gets passed to the +superclass initializer and therefore put into @owner). The second parameter +(and hence the first parameter to the node factory method), +attrname, is a symbol that names the mapping class attribute this +node should map to. It gets stored into @attrname, and the attribute (an +r/w attribute of name attrname) is added to the mapping class (using +attr_accessor).
+ +In the initializer, two option arguments – :optional and :default_value – +are processed in SingleAttributeNode:
+ +Supplying :default_value=>obj makes obj the _default +value_ for this attribute. When unmarshalling (loading) an object from an +XML source, the attribute will be set to this +value if nothing was provided in the XML; when +marshalling (saving), the attribute won't be saved if it is set to the +default value.
+ +Providing just :optional=>true is equivalent to providing +:default_value=>nil.
+ + +# File lib/xml/mapping/base.rb, line 662 +def initialize(*args) + @attrname,*args = super(*args) + @owner.add_accessor @attrname + if @options[:optional] and not(@options.has_key?(:default_value)) + @options[:default_value] = nil + end + initialize_impl(*args) + args +end+
utility method to be used by implementations of extract_attr_value. +Calls the supplied block, catching XML::XXPathError and mapping it to NoAttrValueSet. This is +for the common case that an implementation considers an attribute value not +to be present in the XML if some specific +sub-path does not exist.
+ + + + +# File lib/xml/mapping/base.rb, line 752 +def default_when_xpath_err # :yields: + begin + yield + rescue XML::XXPathError => err + raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})" + end +end+
(to be overridden by subclasses) Extract and return the value of the +attribute this node is responsible for (@attrname) from xml. If +the implementation decides that the attribute value is “unset” in +xml, it should raise NoAttrValueSet in order +to initiate proper handling of possibly supplied :optional and +:default_value options (you may use default_when_xpath_err +for this purpose).
+ + + + +# File lib/xml/mapping/base.rb, line 713 +def extract_attr_value(xml) + raise "abstract method called" +end+
this method was retained for compatibility with xml-mapping 0.8.
+ +It used to be the initializer to be implemented by subclasses. The +arguments (args) are those still unprocessed by SingleAttributeNode's +initializer.
+ +In xml-mapping 0.9 and up, you should just override initialize() and call +super.initialize. The returned array is the same args array.
+ + + + +# File lib/xml/mapping/base.rb, line 679 +def initialize_impl(*args) +end+
(overridden) returns true if and only if the value of this node's +attribute in obj is non-nil.
+ + + + +# File lib/xml/mapping/base.rb, line 761 +def is_present_in? obj + nil != obj.send(:"#{@attrname}") +end+
(to be overridden by subclasses) Write value, which is the current +value of the attribute this node is responsible for (@attrname), into (the +correct sub-nodes, attributes, whatever) of xml.
+ + + + +# File lib/xml/mapping/base.rb, line 734 +def set_attr_value(xml, value) + raise "abstract method called" +end+
Exception that may be used by implementations of extract_attr_value to +announce that the attribute value is not set in the XML and, consequently, the default value +should be set in the object being created, or an Exception be raised if no +default value was specified.
+ +(does somebody have a better name for this class?) base node class that +provides an initializer which lets the user specify a means to +marshal/unmarshal a Ruby object to/from XML. +Used as the base class for nodes that map some sub-nodes of their XML tree to (Ruby-)sub-objects of their +attribute.
+ +processes the keyword arguments :class, :marshaller, and :unmarshaller +(args is ignored). When this initiaizer returns, @marshaller and +@unmarshaller are set to procs that marshal/unmarshal a Ruby object to/from +an XML tree according to the keyword arguments +that were passed to the initializer:
+ +You either supply a :class argument with a class implementing XML::Mapping – in that case, the subtree will be +mapped to an instance of that class (using load_from_xml resp. +fill_into_xml). Or, you supply :marshaller and :unmarshaller arguments +specifying explicit unmarshaller/marshaller procs. The :marshaller proc +takes arguments xml,value and must fill value +(the object to be marshalled) into xml; the :unmarshaller proc +takes xml and must extract and return the object value from it. +Or, you specify none of those arguments, in which case the name of the +class to create will be automatically deduced from the root element name of +the XML node (see XML::Mapping.load_object_from_xml, +XML::Mapping.class_for_root_elt_name).
+ +If both :class and :marshaller/:unmarshaller arguments are supplied, the +latter take precedence.
+ + +# File lib/xml/mapping/standard_nodes.rb, line 101 +def initialize(*args) + args = super(*args) + + @sub_mapping = @options[:sub_mapping] || @mapping + @marshaller, @unmarshaller = @options[:marshaller], @options[:unmarshaller] + + if @options[:class] + unless @marshaller + @marshaller = proc {|xml,value| + value.fill_into_xml xml, :mapping=>@sub_mapping + if xml.unspecified? + xml.name = value.class.root_element_name :mapping=>@sub_mapping + xml.unspecified = false + end + } + end + unless @unmarshaller + @unmarshaller = proc {|xml| + @options[:class].load_from_xml xml, :mapping=>@sub_mapping + } + end + end + + unless @marshaller + @marshaller = proc {|xml,value| + value.fill_into_xml xml, :mapping=>@sub_mapping + if xml.unspecified? + xml.name = value.class.root_element_name :mapping=>@sub_mapping + xml.unspecified = false + end + } + end + unless @unmarshaller + @unmarshaller = proc {|xml| + XML::Mapping.load_object_from_xml xml, :mapping=>@sub_mapping + } + end + + args +end+
Node factory function synopsis:
+ +text_node :_attrname_, _path_ [, :default_value=>_obj_] + [, :optional=>true] + [, :mapping=>_m_]+ +
Node that maps an XML
+node's text (the element's first child text node resp. the
+attribute's value) to a (string) attribute of the mapped object.
+path (an XPath expression) locates the XML node, attrname (a symbol) names the
+attribute. :default_value
is the default value,
+:optional=>true is equivalent to :default_value=>nil (see superclass
+documentation for details). m
is the mapping; it
+defaults to the current default mapping
# File lib/xml/mapping/standard_nodes.rb, line 23 +def initialize(*args) + path,*args = super(*args) + @path = XML::XXPath.new(path) + args +end+
Instances of this class hold (in a pre-compiled form) an XPath pattern. You
+call instance methods like each
, first
,
+all
, create_new
on instances of this class to
+apply the pattern to REXML elements.
create and compile a new XPath. xpathstr is the string +representation (XPath pattern) of the path
+ + + + +# File lib/xml/xxpath.rb, line 21 +def initialize(xpathstr) + @xpathstr = xpathstr # for error messages + + # TODO: write a real XPath parser sometime + + xpathstr='/'+xpathstr if xpathstr[0] != ?/ + + @creator_procs = [ proc{|node,create_new| node} ] + @reader_proc = proc {|nodes| nodes} + + part=nil; part_expected=true + xpathstr.split(/(\/+)/)[1..-1].reverse.each do |x| + if part_expected + part=x + part_expected = false + next + end + part_expected = true + axis = case x + when '/' + :child + when '//' + :descendant + else + raise XXPathError, "XPath (#{xpathstr}): unknown axis: #{x}" + end + axis=:self if axis==:child and (part[0]==?. or part=~/^self::/) # yuck + + step = Step.compile(axis,part) + @creator_procs << step.creator(@creator_procs[-1]) + @reader_proc = step.reader(@reader_proc, @creator_procs[-1]) + end +end+
Return an Enumerable with all sub-nodes of node that match this +XPath. Returns an empty Enumerable if no match was found.
+ +If :ensure_created=>true is provided, all() ensures that a match exists +in node, creating one (and returning it as the sole element of the +returned enumerable) if none existed before.
+ + + + +# File lib/xml/xxpath.rb, line 88 +def all(node,options={}) + raise "options not a hash" unless Hash===options + if options[:create_new] + return [ @creator_procs[-1].call(node,true) ] + else + last_nodes,rest_creator = catch(:not_found) do + return @reader_proc.call([node]) + end + if options[:ensure_created] + [ rest_creator.call(last_nodes[0],false) ] + else + [] + end + end +end+
create a completely new match of this XPath in base_node. +“Completely new” means that a new node will be created for each path +element, even if a matching node already existed in base_node.
+ +path.create_new(node)
is equivalent to
+path.first(node,:create_new=>true)
.
# File lib/xml/xxpath.rb, line 111 +def create_new(base_node) + first(base_node,:create_new=>true) +end+
loop over all sub-nodes of node that match this XPath.
+ + + + +# File lib/xml/xxpath.rb, line 57 +def each(node,options={},&block) + all(node,options).each(&block) +end+
the first sub-node of node that matches this XPath. If nothing +matches, raise XXPathError unless +:allow_nil=>true was provided.
+ +If :ensure_created=>true is provided, first() ensures that a match +exists in node, creating one if none existed before.
+ +path.first(node,:create_new=>true)
is equivalent to
+path.create_new(node)
.
# File lib/xml/xxpath.rb, line 69 +def first(node,options={}) + a=all(node,options) + if a.empty? + if options[:allow_nil] + nil + else + raise XXPathError, "path not found: #{@xpathstr}" + end + else + a[0] + end +end+
attribute node, more or less call-compatible with REXML's Element. +REXML's Attribute class doesn't +provide this…
+ +The all/first calls return instances of this class if they matched an +attribute node.
+ +# File lib/xml/rexml_ext.rb, line 74 +def initialize(parent,name) + @parent,@name = parent,name +end+
# File lib/xml/rexml_ext.rb, line 78 +def self.new(parent,name,create) + if parent.attributes[name] + super(parent,name) + else + if create + parent.attributes[name] = "[unset]" + super(parent,name) + else + nil + end + end +end+
# File lib/xml/rexml_ext.rb, line 100 +def ==(other) + other.kind_of?(Attribute) and other.parent==parent and other.name==name +end+
the value of the attribute.
+ + + + +# File lib/xml/rexml_ext.rb, line 92 +def text + parent.attributes[@name] +end+
# File lib/xml/rexml_ext.rb, line 96 +def text=(x) + parent.attributes[@name] = x +end+
we need a boolean “unspecified?” attribute for XML nodes – paths like “*” oder (somewhen) “foo|bar” +create “unspecified” nodes that the user must then “specify” by setting +their text etc. (or manually setting unspecified=false)
+ +This is mixed into the REXML::Element and XML::XXPath::Accessors::Attribute classes.
+ +# File lib/xml/rexml_ext.rb, line 28 +def self.append_features(base) + return if base.included_modules.include? self # avoid aliasing methods more than once + # (would lead to infinite recursion) + super + base.module_eval <<-EOS + alias_method :_text_orig, :text + alias_method :_textis_orig, :text= + def text + # we're suffering from the "fragile base class" + # phenomenon here -- we don't know whether the + # implementation of the class we get mixed into always + # calls text (instead of just accessing @text or so) + if unspecified? + "[UNSPECIFIED]" + else + _text_orig + end + end + def text=(x) + _textis_orig(x) + self.unspecified=false + end + + alias_method :_nameis_orig, :name= + def name=(x) + _nameis_orig(x) + self.unspecified=false + end + EOS +end+
# File lib/xml/rexml_ext.rb, line 24 +def unspecified=(x) + @xml_xpath_unspecified = x +end+
# File lib/xml/rexml_ext.rb, line 20 +def unspecified? + @xml_xpath_unspecified ||= false +end+
set of convenience wrappers around XML::XPath's instance methods, for +people who frequently use XML::XPath directly. This module is included into +the REXML node classes and adds methods to them +that enable you to write a call like
+ +path.first(xml_element) ++ +
as the more pleasant-looking variant
+ +xml_element.first_xpath(path) ++ +
with the added bonus that path may not only be an XML::XXPath instance, but also just a String containing the XPath expression.
+ +Please note that the names of all the added methods are suffixed with +“_xpath” to avoid name clashes with REXML +methods. Please note also that this was changed recently, so older versions +of xml-mapping (version < 0.9) used method names without _xpath appended +and thus would be incompatible with this one here.
+ +As a special convenience, if you're using an older version of REXML that doesn't have the new methods yet, +methods without _xpath in their names will also (additionally) be added to +the REXML classes. This will enable code that +relied on the old names to keep on working as long as REXML isn't updated, at which point that code +will fail and must be changed to used the methods suffixed with _xpath.
+ +see XML::XXPath#all
+ + + + +# File lib/xml/xxpath_methods.rb, line 47 +def all_xpath(path,options={}) + to_xxpath(path).all self, options +end+
# File lib/xml/xxpath_methods.rb, line 52 +def create_new_xpath(path) + to_xxpath(path).create_new self +end+
see XML::XXPath#each
+ + + + +# File lib/xml/xxpath_methods.rb, line 37 +def each_xpath(path,options={},&block) + to_xxpath(path).each self, options, &block +end+
# File lib/xml/xxpath_methods.rb, line 42 +def first_xpath(path,options={}) + to_xxpath(path).first self, options +end+
# File lib/xml/xxpath_methods.rb, line 75 +def to_xxpath(path) + if String===path + XXPath.new path + else + path + end +end+
At the lowest level, the “Accessors” sub-module contains reader and creator +functions that correspond to the various types of path elements +(elt_name, @attr_name, elt_name etc.) that xml-xxpath +supports. A reader function gets an array of nodes and the search +parameters corresponding to its path element type (e.g. elt_name, +attr_name, attr_value) and returns an array with all +matching direct sub-nodes of any of the supplied nodes. A creator function +gets one node and the search parameters and returns the created sub-node.
+ +An XPath expression
+<things1>/<things2>/.../<thingsx>
is
+compiled into a bunch of nested closures, each of which is responsible for
+a specific path element and calls the corresponding accessor function:
@creator_procs
– an array of “creator” functions.
+@creator_procs[i]
gets passed a base node (XML element) and a
+create_new flag, and it creates the path
+<things[x-i+1]>/<things[x-i+2]>/.../<thingsx>
+inside the base node and returns the hindmost element created (i.e. the one
+corresponding to <thingsx>
).
@reader_proc
– a “reader” function that gets passed an array
+of nodes and returns an array of all nodes that matched the path in any of
+the supplied nodes, or, if no match was found, throws :not_found along with
+the last non-empty set of nodes that was found, and the element of
+@creator_procs
that could be used to create the remaining part
+of the path.
The all
function is then trivially implemented on top of this:
def all(node,options={}) + raise "options not a hash" unless Hash===options + if options[:create_new] + return [ @creator_procs[-1].call(node,true) ] + else + last_nodes,rest_creator = catch(:not_found) do + return @reader_proc.call([node]) + end + if options[:ensure_created] + [ rest_creator.call(last_nodes[0],false) ] + else + [] + end + end +end ++ +
…and first
, create_new
etc. are even more trivial
+frontends to that.
The implementations of the @creator_procs
look like this:
@creator_procs[0] = + proc{|node,create_new| node} + +@creator_procs[1] = + proc {|node,create_new| + @creator_procs[0].call(Accessors.create_subnode_by_<thingsx>(node,create_new,<thingsx>), + create_new) + } + +@creator_procs[2] = + proc {|node,create_new| + @creator_procs[1].call(Accessors.create_subnode_by_<thingsx-1>(node,create_new,<thingsx-1>), + create_new) + } + +... + +@creator_procs[n] = + proc {|node,create_new| + @creator_procs[n-1].call(Accessors.create_subnode_by_<things[x+1-n]>(node,create_new,<things[x+1-n]>), + create_new) + } + +... +@creator_procs[x] = + proc {|node,create_new| + @creator_procs[x-1].call(Accessors.create_subnode_by_<things1>(node,create_new,<things1>), + create_new) + }+ +
..and the implementation of @reader_proc looks like this:
+ +@reader_proc = rpx where + +rp0 = proc {|nodes| nodes} + +rp1 = proc {|nodes| + next_nodes = Accessors.subnodes_by_<thingsx>(nodes,<thingsx>) + if (next_nodes == []) + throw :not_found, [nodes,@creator_procs[1]] + else + rp0.call(next_nodes) + end + } + +rp2 = proc {|nodes| + next_nodes = Accessors.subnodes_by_<thingsx-1>(nodes,<thingsx-1>) + if (next_nodes == []) + throw :not_found, [nodes,@creator_procs[2]] + else + rp1.call(next_nodes) + end + } +... + +rpx = proc {|nodes| + next_nodes = Accessors.subnodes_by_<things1>(nodes,<things1>) + if (next_nodes == []) + throw :not_found, [nodes,@creator_procs[x]] + else + rpx-1.call(next_nodes) + end + }+
Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.
+ +For downloading the latest version, git repository access etc. go to:
+ +github.com/multi-io/xml-mapping
+ +(example document stolen + extended from www.castor.org/xml-mapping.html)
+ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<Order reference="12343-AHSHE-314159"> + <Client> + <Name>Jean Smith</Name> + <Address where="home"> + <City>San Mateo</City> + <State>CA</State> + <ZIP>94403</ZIP> + <Street>2000, Alameda de las Pulgas</Street> + </Address> + <Address where="work"> + <City>San Francisco</City> + <State>CA</State> + <ZIP>94102</ZIP> + <Street>98765, Fulton Street</Street> + </Address> + </Client> + + <Item reference="RF-0001"> + <Description>Stuffed Penguin</Description> + <Quantity>10</Quantity> + <UnitPrice>8.95</UnitPrice> + </Item> + + <Item reference="RF-0034"> + <Description>Chocolate</Description> + <Quantity>5</Quantity> + <UnitPrice>28.50</UnitPrice> + </Item> + + <Item reference="RF-3341"> + <Description>Cookie</Description> + <Quantity>30</Quantity> + <UnitPrice>0.85</UnitPrice> + </Item> + + <Signed-By> + <Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + </Signature> + + <Signature> + <Name>Jill Smith</Name> + <Position>clerk</Position> + </Signature> + + <Signature> + <Name>Miles O'Brien</Name> + </Signature> + </Signed-By> + +</Order>+ +
require 'xml/mapping' + +## forward declarations +class Client; end +class Address; end +class Item; end +class Signature; end + + +class Order + include XML::Mapping + + text_node :reference, "@reference" + object_node :client, "Client", :class=>Client + hash_node :items, "Item", "@reference", :class=>Item + array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] + + def total_price + items.values.map{|i| i.total_price}.inject(0){|x,y|x+y} + end +end + + +class Client + include XML::Mapping + + text_node :name, "Name" + object_node :home_address, "Address[@where='home']", :class=>Address + object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil +end + + +class Address + include XML::Mapping + + text_node :city, "City" + text_node :state, "State" + numeric_node :zip, "ZIP" + text_node :street, "Street" +end + + +class Item + include XML::Mapping + + text_node :descr, "Description" + numeric_node :quantity, "Quantity" + numeric_node :unit_price, "UnitPrice" + + def total_price + quantity*unit_price + end +end + + +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" +end ++ +
####read access +o=Order.load_from_file("order.xml") +=> #<Order:0x007ff64a0fe8b0 @signatures=[#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">], @reference="12343-AHSHE-314159", @client=#<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>, @items={"RF-0001"=>#<Item:0x007ff64a0df550 @descr="Stuffed Penguin", @quantity=10, @unit_price=8.95>, "RF-0034"=>#<Item:0x007ff64a0ddbd8 @descr="Chocolate", @quantity=5, @unit_price=28.5>, "RF-3341"=>#<Item:0x007ff64a0dc0d0 @descr="Cookie", @quantity=30, @unit_price=0.85>}> +o.reference +=> "12343-AHSHE-314159" +o.client +=> #<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">> +o.items.keys +=> ["RF-0001", "RF-0034", "RF-3341"] +o.items["RF-0034"].descr +=> "Chocolate" +o.items["RF-0034"].total_price +=> 142.5 +o.signatures +=> [#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">] +o.signatures[2].name +=> "Miles O'Brien" +o.signatures[2].position +=> "Some Employee" +## default value was set + +o.total_price +=> 257.5 + +####write access +o.client.name="James T. Kirk" +o.items['RF-4711'] = Item.new +o.items['RF-4711'].descr = 'power transfer grid' +o.items['RF-4711'].quantity = 2 +o.items['RF-4711'].unit_price = 29.95 + +s=Signature.new +s.name='Harry Smith' +s.position='general manager' +o.signatures << s +xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) +=> <order reference='12343-AHSHE-314159'> ... </> +xml.write($stdout,2) +<order reference='12343-AHSHE-314159'> + <Client> + <Name> + James T. Kirk + </Name> + <Address where='home'> + <City> + San Mateo + </City> + <State> + CA + </State> + <ZIP> + 94403 + </ZIP> + <Street> + 2000, Alameda de las Pulgas + </Street> + </Address> + <Address where='work'> + <City> + San Francisco + </City> + <State> + CA + </State> + <ZIP> + 94102 + </ZIP> + <Street> + 98765, Fulton Street + </Street> + </Address> + </Client> + <Item reference='RF-0001'> + <Description> + Stuffed Penguin + </Description> + <Quantity> + 10 + </Quantity> + <UnitPrice> + 8.95 + </UnitPrice> + </Item> + <Item reference='RF-0034'> + <Description> + Chocolate + </Description> + <Quantity> + 5 + </Quantity> + <UnitPrice> + 28.5 + </UnitPrice> + </Item> + <Item reference='RF-3341'> + <Description> + Cookie + </Description> + <Quantity> + 30 + </Quantity> + <UnitPrice> + 0.85 + </UnitPrice> + </Item> + <Item reference='RF-4711'> + <Description> + power transfer grid + </Description> + <Quantity> + 2 + </Quantity> + <UnitPrice> + 29.95 + </UnitPrice> + </Item> + <Signed-By> + <Signature> + <Name> + John Doe + </Name> + <Position> + product manager + </Position> + </Signature> + <Signature> + <Name> + Jill Smith + </Name> + <Position> + clerk + </Position> + </Signature> + <Signature> + <Name> + Miles O'Brien + </Name> + </Signature> + <Signature> + <Name> + Harry Smith + </Name> + <Position> + general manager + </Position> + </Signature> + </Signed-By> +</order> + + +####Starting a new order from scratch +o = Order.new +=> #<Order:0x007ff64a206050 @signatures=[]> +## attributes with default values (here: signatures) are set +## automatically + +xml=o.save_to_xml +XML::MappingError: no value, and no default value, for attribute: reference + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:724:in `obj_to_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:218:in `block in fill_into_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `each' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `fill_into_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:229:in `save_to_xml' +## can't save as long as there are still unset attributes without +## default values + +o.reference = "FOOBAR-1234" + +o.client = Client.new +o.client.name = 'Ford Prefect' +o.client.home_address = Address.new +o.client.home_address.street = '42 Park Av.' +o.client.home_address.city = 'small planet' +o.client.home_address.zip = 17263 +o.client.home_address.state = 'Betelgeuse system' + +o.items={'XY-42' => Item.new} +o.items['XY-42'].descr = 'improbability drive' +o.items['XY-42'].quantity = 3 +o.items['XY-42'].unit_price = 299.95 + +xml=o.save_to_xml +xml.write($stdout,2) + +<order reference='FOOBAR-1234'> + <Client> + <Name> + Ford Prefect + </Name> + <Address where='home'> + <City> + small planet + </City> + <State> + Betelgeuse system + </State> + <ZIP> + 17263 + </ZIP> + <Street> + 42 Park Av. + </Street> + </Address> + </Client> + <Item reference='XY-42'> + <Description> + improbability drive + </Description> + <Quantity> + 3 + </Quantity> + <UnitPrice> + 299.95 + </UnitPrice> + </Item> +</order> +## the root element name when saving an object to XML will by default +## be derived from the class name (in this example, "Order" became +## "order"). This can be overridden on a per-class basis; see +## XML::Mapping::ClassMethods#root_element_name for details.+ +
As shown in the example, you have to include XML::Mapping into a class to turn it into a +“mapping class”. There are no other restrictions imposed on mapping +classes; you can add attributes and methods to them, include additional +modules in them, derive them from other classes, derive other classes from +them etc.pp.
+ +An instance of a mapping class can be created from/converted into an XML node with methods like XML::Mapping::ClassMethods#load_from_xml, +XML::Mapping#save_to_xml, +XML::Mapping::ClassMethods#load_from_file, +XML::Mapping#save_to_file. +Special class methods like “text_node”, “array_node” etc., called +node factory methods, may be called from the +body of the class definition to define instance attributes that are +automatically and bidirectionally mapped to subtrees of the XML element an instance of the class is mapped to.
+ +For example, in the definition
+ +class Address + include XML::Mapping + + text_node :city, "City" + text_node :state, "State" + numeric_node :zip, "ZIP" + text_node :street, "Street" +end ++ +
the first call to text_node creates an attribute named “city” which is
+mapped to the text of the XML child element defined
+by the XPath expression “City” (xml-mapping includes an XPath interpreter
+that can also be used seperately; see below). When
+you create an instance of Address
from an XML element (using Address.load_from_file(file_name) or
+Address.load_from_xml(rexml_element)), that instance's “city” attribute
+will be set to the text of the XML element's
+“City” child element. When you convert an instance of Address
+into an XML element, a sub-element “City” is added
+and its text is set to the current value of the city
+attribute. The other node types (numeric_node, array_node etc.) work
+analogously. Generally said, when an instance of the above
+Address
class is created from or converted to an XML tree, each of the four nodes in the class maps some
+parts of that XML tree to a single, specific
+attribute of the Adress
instance. The name of that attribute
+is given in the first argument to the node factory method. Such a node is
+called a “single-attribute node”. All node types that come with xml-mapping
+except one (choice_node
, which I'll talk about below) are
+single-attribute nodes.
For each single-attribute node you may define a default value +which will be set if there was no value defined for the attribute in the XML source.
+ +From the example:
+ +class Signature + include XML::Mapping + + text_node :position, "Position", :default_value=>"Some Employee" +end ++ +
The semantics of default values are as follows:
+when creating a new instance from scratch:
+attributes with default values are set to their default values
+attributes without default values are left unset
+(when defining your own initializer, you'll have to call the inherited +initialize method in order to get this behaviour)
+when loading an instance from an XML document:
+ +when saving an instance to an XML document:
+unset attributes without default values raise an error
+attributes with default values that are set to their default values are +not saved
+all other attributes are saved
+This implies that:
+attributes that are set to their respective default values are not +represented in the XML
+attributes without default values must be set explicitly before saving
+Single-attribute nodes of type array_node
,
+hash_node
, and object_node
recursively map one or
+more subtrees of their XML to sub-objects (e.g.
+array elements or hash values) of their attribute. For example, with the
+line
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] ++ +
, an attribute named “signatures” is added to the surrounding class (here:
+Order
); the attribute will be an array whose elements
+correspond to the XML sub-trees yielded by the XPath
+expression “Signed-By/Signature” (relative to the tree corresponding to the
+Order
instance). Each element will be of class
+Signature
(internally, each element is created from its
+corresponding XML subtree by just calling
+Signature.load_from_xml(the_subtree)
). The reason why the path
+“Signed-By/Signature” is provided in two arguments instead of just one
+combined one becomes apparent when marshalling the array (along with the
+surrounding Order
object) back into a sequence of XML elements. When that happens, “Signed-By” names the
+common base element for all those elements, and “Signature” is the path
+that will be duplicated for each element. For example, when the
+signatures
attribute contains an array with 3
+Signature
instances (let's call them sig1
,
+sig2
, and sig3
) in it, it will be marshalled to
+an XML tree that looks like this:
<Signed-By> + <Signature> + [marshalled object sig1] + </Signature> + <Signature> + [marshalled object sig2] + </Signature> + <Signature> + [marshalled object sig3] + </Signature> +</Signed-By>+ +
Internally, each Signature
instance is stored into its
+<Signature>
sub-element by calling
+the_signature_instance.fill_into_xml(the_sub_element)
. The
+input document in the example above shows how this ends up looking.
hash_node
s work similarly, but they define hash-valued
+attributes instead of array-valued ones.
object_node
s are the simplest of the three types of
+single-attribute nodes with sub-objects. They just map a single given
+subtree directly to their attribute value. See the example for examples :)
The mentioned methods load_from_xml
and
+fill_into_xml
are the only methods classes must implement in
+order to be usable in the :class=>
keyword arguments to
+node factory methods. Mapping classes (i.e. classes that include
+XML::Mapping
) automatically inherit those functions and can thus be
+readily used in :class=>
arguments, as shown for the
+Signature
class in the array_node
call above. In
+addition to that, xml-mapping adds those methods to some of Ruby's core
+classes, namely String
and Numeric
(and thus
+Float
, Integer
, and BigInt
). So you
+can also use strings or numbers as sub-objects of attributes of
+array_node
, hash_node
, or
+object_node
nodes. For example, say you have an XML document like this one:
<?xml version="1.0" encoding="ISO-8859-1"?> + +<people> + <names> + <name>Jim</name> + <name>Susan</name> + <name>Herbie</name> + <name>Nancy</name> + </names> +</people>+ +
, and you want to map all the names to a string array attribute
+names
, you could do it like this:
require 'xml/mapping' +class People + include XML::Mapping + array_node :names, "names", "name", :class=>String +end ++ +
usage:
+ +ppl=People.load_from_file("stringarray.xml") +=> #<People:0x007ff64a0cda08 @names=["Jim", "Susan", "Herbie", "Nancy"]> +ppl.names +=> ["Jim", "Susan", "Herbie", "Nancy"] + +ppl.names.concat ["Mary","Arnold"] +=> ["Jim", "Susan", "Herbie", "Nancy", "Mary", "Arnold"] +ppl.save_to_xml.write $stdout,2 + +<people> + <names> + <name> + Jim + </name> + <name> + Susan + </name> + <name> + Herbie + </name> + <name> + Nancy + </name> + <name> + Mary + </name> + <name> + Arnold + </name> + </names> +</people>+ +
As a side node, this feature actually makes text_node
and
+numeric_node
special cases of object_node
. For
+example, text_node :attr, "path"
is the same as
+object_node :attr, "path", :class=>String
.
Besides the :class
keyword argument, there are alternative
+ways for a single-attribute node with sub-objects to specify the way the
+sub-objects are created from/marshalled into their subtrees.
First, it's possible not to specify anything at all – in that case, the +class of a sub-object will be automatically deduced from the root element +name of its subtree. This allows you to achieve a kind of “polymorphic”, +late-bound way to decide about the sub-object's class. The following +example document contains a hierarchical, recursive set of named +“documents” and “folders”, where folders hold a set of entries, each of +which may again be either a document or a folder:
+ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<folder name="home"> + <document name="plan"> + <contents> inhale, exhale</contents> + </document> + + <folder name="work"> + <folder name="xml-mapping"> + <document name="README"> + <contents>foo bar baz</contents> + </document> + </folder> + </folder> + +</folder>+ +
This can be mapped to Ruby like this:
+ +require 'xml/mapping' + +class Entry + include XML::Mapping + + text_node :name, "@name" +end + + +class Document <Entry + include XML::Mapping + + text_node :contents, "contents" +end + + +class Folder <Entry + include XML::Mapping + + array_node :entries, "document|folder", :default_value=>[] + + def [](name) + entries.select{|e|e.name==name}[0] + end + + def append(name,entry) + entries << entry + entry.name = name + entry + end +end ++ +
Usage:
+ +root = XML::Mapping.load_object_from_file "documents_folders.xml" +=> #<Folder:0x007ff6499c0f58 @entries=[#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">], @name="home"> +root.name +=> "home" +root.entries +=> [#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">] + +root.append "etc", Folder.new +root["etc"].append "passwd", Document.new +root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh" +root["etc"].append "hosts", Document.new +root["etc"]["hosts"].contents = "127.0.0.1 localhost" + +xml = root.save_to_xml +=> <folder name='home'> ... </> +xml.write $stdout,2 + +<folder name='home'> + <document name='plan'> + <contents> + inhale, exhale + </contents> + </document> + <folder name='work'> + <folder name='xml-mapping'> + <document name='README'> + <contents> + foo bar baz + </contents> + </document> + </folder> + </folder> + <folder name='etc'> + <document name='passwd'> + <contents> + foo:x:2:2:/bin/sh + </contents> + </document> + <document name='hosts'> + <contents> + 127.0.0.1 localhost + </contents> + </document> + </folder> +</folder>+ +
As you see, the Folder#entries
attribute is mapped via an
+array_node that does not specify a :class
or anything else to
+govern the instantiation of the array's elements. This causes
+xml-mapping to deduce the class of each array element from the root element
+name of the corresponding XML tree. In this example,
+the root element name is either “document” or “folder”. The mapping between
+root element names and class names is the one briefly described in example at the beginning of this document – the
+unqualified class name is just converted to lower case and “dashed”, e.g.
+Foo::Bar::MyClass becomes “my-class”; and you may overwrite this on a
+per-class basis by calling root_element_name
+"the-new-name"
in the class body. In our example, the root
+element name “document” leads to an instantiation of class
+Document
, and the root element name “folder” leads to an
+instantiation of class Folder
.
Incidentally, the last example shows that you can readily derive mapping
+classes from one another (as said before, you can also derive mapping
+classes from other classes, include other modules into them etc. at will).
+This works just like intuition thinks it should – when deriving one mapping
+class from another one, the list of nodes in effect when loading/saving
+instances of the derived class will consist of all nodes of that class and
+all superclasses, starting with the topmost superclass that has nodes
+defined. There is one thing to take care of though: When deriving mapping
+classes from one another, you have to make sure to include
+XML::Mapping
in each class. This requirement exists purely due to
+ease-of-implementation considerations; there are probably ways to do away
+with it, but the inconvenience seemed not severe enough for me to bother
+(as yet). Still, you might get “strange” errors if you forget to do it for
+a class.
Besides the :class
keyword argument and no argument, there is
+a third way to specify the way the sub-objects are created from/marshalled
+into their subtrees: :marshaller
and/or
+:unmarshaller
keyword arguments. Here you pass procs in which
+you just do all the work manually. So this is basically a “catch-all” for
+cases where the other two alternatives are not appropriate for the problem
+at hand. (TODO: Use other example?) Let's say we want to
+extend the Signature
class from the initial example to include
+the date on which the signature was created. We want the new XML representation of such a signature to look like
+this:
<Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + <signed-on> + <day>13</day> + <month>2</month> + <year>2005</year> + </signed-on> +</Signature>+ +
So, a new “signed-on” element was added that holds the day, month, and
+year. In the Signature
instance in Ruby, we want the date to
+be stored in an attribute named signed_on
of type
+Time
(that's Ruby's built-in Time
class).
One could think of using object_node
, but something like
+object_node :signed_on, "signed-on", :class=>Time
+won't work because Time
isn't a mapping class and
+doesn't define methods load_from_xml
and
+fill_into_xml
(we could easily define those though; we'll
+talk about that possibility here and here). The fastest, most ad-hoc way to
+achieve what we want are :marshaller and :unmarshaller keyword arguments,
+like this:
require 'xml/mapping' +require 'xml/xxpath_methods' + +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" + object_node :signed_on, "signed-on", + :unmarshaller=>proc{|xml| + y,m,d = [xml.first_xpath("year").text.to_i, + xml.first_xpath("month").text.to_i, + xml.first_xpath("day").text.to_i] + Time.local(y,m,d) + }, + :marshaller=>proc{|xml,value| + e = xml.elements.add; e.name = "year"; e.text = value.year + e = xml.elements.add; e.name = "month"; e.text = value.month + e = xml.elements.add; e.name = "day"; e.text = value.day + + # xml.first("year",:ensure_created=>true).text = value.year + # xml.first("month",:ensure_created=>true).text = value.month + # xml.first("day",:ensure_created=>true).text = value.day + } +end ++ +
The :unmarshaller
proc will be called whenever a
+Signature
instance is being read in from an XML source. The xml
argument passed to the
+proc contains (as a REXML::Element
+instance) the XML subtree corresponding to the
+node's attribute's sub-object currently being read. In the case of
+our object_node
, the sub-object is just the node's
+attribute (signed_on
) itself, and the subtree is the one
+rooted at the <signed-on> element (if this were e.g. an
+array_node
, the :unmarshaller
proc would be
+called once for each array element, and xml
would hold the
+subtree corresponding to the “current” array element). The proc is expected
+to extract the sub-object's data from xml
and return the
+sub-object. So we have to read the “year”, “month”, and “day” elements,
+construct a Time
instance from them and return that. One could
+just use the REXML API to do that, but I've
+decided here to use the XPath interpreter that comes with xml-mapping
+(xml/xxpath), and specifically the 'xml/xxpath_methods' utility
+library that adds methods like first
to REMXML::Element. We
+call first
on xml
three times, passing XPath
+expressions to extract the “year”/“month”/“day” sub-elements, construct the
+Time
instance from that and return it. The XPath library is
+explained in more detail below.
The :marshaller
proc will be called whenever a
+Signature
instance is being written into an XML tree. xml
is again the XML subtree rooted at the <signed-on> element (it
+will still be empty when this proc is called), and value
is
+the current value of the sub-object (again, since this is an
+object_node
, value
is the node's attribute,
+i.e. the Time
instance). We have to fill xml
with
+the data from value
here. So we add three elements “year”,
+“month” and “day” and set their texts to the corresponding values from
+value
. The commented-out code shows an alternative
+implementation of the same thing using the XPath interpreter.
It should be mentioned again that :marshaller/:unmarshaller procs are
+possible with all single-attribute nodes with sub-objects, i.e. with
+object_node
, array_node
, and
+hash_node
. So, if you wanted to map a whole array of date
+values, you could use array_node
with the same
+:marshaller/:unmarshaller procs as above, for example:
array_node :birthdays, "birthdays", "birthday", + :unmarshaller=> <as above>, + :marshaller=> <as above>+ +
You can see that :marshaller/:unmarshaller procs give you more flexibility,
+but they also impose more work because you essentially have to do all the
+work of marshalling/unmarshalling the sub-objects yourself. If you find
+yourself copying and pasting marshaller/unmarshaller procs all over the
+place, you should instead define your own node type or mix the
+marshalling/unmarshalling capabilities into the Time
class
+itself. This is explained here and here, and you'll see that it's not
+really much more work than writing :marshaller and :unmarshaller procs (you
+essentially just move the code from those procs into your own node type
+resp. into the Time
class), so you should not hesitate to do
+this.
Another thing worth mentioning is that you don't have to specify
+both a :marshaller and an :unmarshaller simultaneously. You can as
+well give only one of them, and in addition to that pass a
+:class
argument or no argument. When you do that, the
+specified marshaller (or unmarshaller) will be used when marshalling (resp.
+unmarshalling) the sub-objects, and the other passed argument
+(:class
or none) will be employed when unmarshalling (resp.
+marshalling) the sub-objects. So, in effect, you can deactivate or
+“short-cut” some part of the marshalling/unmarshalling functionality of a
+node type while retaining another part.
I'll shed some more light on how single-attribute nodes add mapped +attributes to Ruby classes. An attribute declaration like
+ +text_node :city, "City" ++ +
maps some portion of the XML tree (here: the “City”
+sub-element) to an attribute (here: “city”) of the class whose body the
+declaration appears in. When writing (marshalling) instances of the
+surrounding class into an XML document, xml-mapping
+will read the attribute value from the instance using the function named
+city
; when reading (unmarshalling) an instance from an XML document, xml-mapping will use the one-parameter
+function city=
to set the attribute in the instance to the
+value read from the XML document.
If these functions don't exist at the time the node declaration is
+executed, xml-mapping adds default implementations that simply read/write
+the attribute value to instance variables that have the same name as the
+attribute. For example, the city
attribute declaration in the
+Address
class in the example added functions city
+and city=
that read/write from/to the instance variable
+@city
.
If, however, these functions already exist prior to defining the +attributes, xml-mapping will leave them untouched, so your precious +self-written accessor methods that do whatever complicated internal +processing of the data won't be overwritten.
+ +This means that you can not only create new mapping classes from scratch, +you can also take existing classes that contain some “business logic” and +“augment” them with xml-mapping capabilities. As a simple example, +let's augment Ruby's “Time” class with node declarations that +declare XML mappings for the day, month etc. fields:
+ +class Time + include XML::Mapping + + numeric_node :year, "year" + numeric_node :month, "month" + numeric_node :day, "mday" + numeric_node :hour, "hours" + numeric_node :min, "minutes" + numeric_node :sec, "seconds" +end + + +nowxml=Time.now.save_to_xml +=> <time> ... </> +nowxml.write($stdout,2) +<time> + <year> + 2015 + </year> + <month> + 3 + </month> + <mday> + 1 + </mday> + <hours> + 15 + </hours> + <minutes> + 31 + </minutes> + <seconds> + 6 + </seconds> +</time>+ +
Here XML mappings are defined for the existing
+fields year
, month
etc. Xml-mapping noticed that
+the getter methods for those attributes existed, so it didn't overwrite
+them. When calling save_to_xml
on a Time
object,
+these methods are called and return the object's values for those
+fields, which then get written to the output XML.
So you can convert Time
objects into XML trees. What about reading them back in from XML? All XML reading operations
+go through <Class>.load_from_xml
. The
+load_from_xml
class method inherited from XML::Mapping (see XML::Mapping::ClassMethods#load_from_xml)
+allocates a new instance of the class (Time
), then calls
+fill_from_xml
(i.e. XML::Mapping#fill_from_xml)
+on it. fill_from_xml
iterates over all our nodes in the order
+of their definition. For each node, its data (the <year>, or
+<month>, or <day> etc. element) is read from the XML source and then written to the Time
+instance via the respective setter method (year=
,
+month=
, day=
etc.). These methods didn't
+exist in Time
before (Time
objects are
+immutable), so xml-mapping defined its own, default setter methods that
+just set @year
, @month
etc. This is of course
+pretty useless because Time
objects don't hold their time
+in these variables, so the setter methods don't really change the time
+of the Time
object. So we have to redefine
+load_from_xml
for the Time
class:
def Time.load_from_xml(xml, options={:mapping=>:_default}) + year,month,day,hour,min,sec = + [xml.first_xpath("year").text.to_i, + xml.first_xpath("month").text.to_i, + xml.first_xpath("mday").text.to_i, + xml.first_xpath("hours").text.to_i, + xml.first_xpath("minutes").text.to_i, + xml.first_xpath("seconds").text.to_i] + Time.local(year,month,day,hour,min,sec) +end ++ +
All nodes I've shown so far (node types text_node, numeric_node, +boolean_node, object_node, array_node, and hash_node) were single-attribute +nodes: The first parameter to the node factory method of such a node is an +attribute name, and the attribute of that name is the only piece of the +state of instances of the node's mapping class that gets read/written +by the node.
+ +There is one node type distributed with xml-mapping that is not a
+single-attribute node: choice_node
. A choice_node
+allows you to specify a sequence of pairs, each consisting of an XPath
+expression and another node (any node is supported here, including other
+choice_nodes). When reading in an XML source, the
+choice_node will delegate the work to the first node in the sequence whose
+corresponding XPath expression was matched in the XML. When writing an object back to XML, the choice_node will delegate the work to the
+first node whose data was “present” in the object (for single-attribute
+nodes, the data is considered “present” if the node's attribute is
+non-nil; for choice_nodes, the data is considered “present” if at least one
+of the node's sub-nodes is “present”).
As a (somewhat contrived) example, here's a mapping for
+Publication
objects that have either a single author
+(contained in an “author” XML attribute) or several
+“contributors” (contained in a sequence of “contr” XML elements):
class Publication + include XML::Mapping + + choice_node :if, '@author', :then, (text_node :author, '@author'), + :elsif, 'contr', :then, (array_node :contributors, 'contr', :class=>String) +end + +### usage + +p1 = Publication.load_from_xml(REXML::Document.new('<publication author="Jim"/>').root) +=> #<Publication:0x007ff64a166a78 @author="Jim"> + +p2 = Publication.load_from_xml(REXML::Document.new(' +<publication> + <contr>Chris</contr> + <contr>Mel</contr> + <contr>Toby</contr> +</publication>').root) +=> #<Publication:0x007ff64a155f48 @contributors=["Chris", "Mel", "Toby"]>+ +
The symbols :if, :then, and :elsif (but not :else – see below) in the
+choice_node
's node factory method call are ignored; they
+may be sprinkled across the argument list at will (preferably the way shown
+above of course) to increase readability.
The rest of the arguments specify the mentioned sequence of XPath +expressions and corresponding nodes.
+ +When reading a Publication
object from XML, the XPath expressions from the
+choice_node
(@author
and contr
) will
+be matched in sequence against the source XML tree
+until a match is found or the end of the argument list is reached. If the
+end is reached, an exception is raised. Otherwise, for the first XPath
+expression that matched, the corresponding node will be invoked (i.e. used
+to read actual data from the XML source into the
+Person
object). If you specify :else, :default, or :otherwise
+in place of an XPath expression, this is treated as an XPath expression
+that always matches. So you can use :else (or :default or :otherwise) for a
+“fallback” node that will be used if none of the other XPath expressions
+matched (an example for this follows).
When writing a Publication
object back to XML, the first node in the sequence whose data is
+“present” in the source object will be invoked to write data from the
+object into the target XML tree (and the
+corresponding XPath expression will be created in the XML tree if it doesn't exist already). If there is
+no such node in the sequence, an exception is raised. As said above, for
+single-attribute nodes, the node's data is considered “present” if the
+node's attribute is non-nil. So, if you write a
+Publication
object to XML, and either
+the author
or the contributors
attribute of the
+object is set, it will be written; if both attributes are nil, an exception
+will be raised.
A frequent use case for choice_nodes will probably be object attributes
+that may be represented in multiple alternative ways in XML. As an example, consider “Person” objects where the
+name of the person should be stored alternatively in a sub-element named
+name
, or an attribute named name
, or in the text
+of the person
element itself. You can achieve this with
+choice_node
like this:
class Person + include XML::Mapping + + choice_node :if, 'name', :then, (text_node :name, 'name'), + :elsif, '@name', :then, (text_node :name, '@name'), + :else, (text_node :name, '.') +end + +### usage + +p1 = Person.load_from_xml(REXML::Document.new('<person name="Jim"/>').root) +=> #<Person:0x007ff64a1cd660 @name="Jim"> + +p2 = Person.load_from_xml(REXML::Document.new('<person><name>James</name></person>').root) +=> #<Person:0x007ff64a1c54b0 @name="James"> + +p3 = Person.load_from_xml(REXML::Document.new('<person>Suzy</person>').root) +=> #<Person:0x007ff64a1b6820 @name="Suzy"> + + +p1.save_to_xml.write($stdout) +<person><name>Jim</name></person> +p2.save_to_xml.write($stdout) +<person><name>James</name></person> +p3.save_to_xml.write($stdout) +<person><name>Suzy</name></person>+ +
Here all sub-nodes of the choice_nodes are single-attribute nodes
+(text_nodes) with the same attribute (name
). As you see, when
+writing persons to XML, the name is always stored in
+a <name> sub-element. Of course, this is because that alternative
+appears first in the choice_node.
Finally, all nodes support keyword arguments :reader and :writer +which allow you to extend or completely override the reading and/or writing +functionality of the node with your own code. The :reader as well as the +:writer argument must be a proc that takes as its arguments the Ruby object +to be read/written (instance of the mapping class the node belongs to) and +the XML tree to be written to/read from. An optional +third argument may be specified – it will receive a proc that wraps the +default reader/writer functionality of the node.
+ +The :reader proc is for reading (from the XML into +the object), the :writer proc is for writing (from the object into the XML).
+ +Here's a (really contrived) example:
+ +class Foo + include XML::Mapping + + text_node :name, "@name", :reader=>proc{|obj,xml,default_reader| + default_reader.call(obj,xml) + obj.name += xml.attributes['more'] + }, + :writer=>proc{|obj,xml| + xml.attributes['bar'] = "hi #{obj.name} ho" + } +end + +f = Foo.load_from_xml(REXML::Document.new('<foo name="Jim" more="XYZ"/>').root) +=> #<Foo:0x007ff64a10e8c8 @name="JimXYZ"> + +xml = f.save_to_xml +xml.write $stdout,2 +<foo bar='hi JimXYZ ho'/>+ +
So there's a “Foo” class with a text_node that would by default
+(without the :reader and :writer proc) map the Ruby attribute “name” to the
+XML attribute “name”. The :reader proc is invoked
+when reading from XML into a Foo
+object. The xml
argument is the XML
+tree, obj
is the object. default_reader
is the
+proc that wraps the default reading functionality of the node. We invoke it
+at the beginning. For this text_node, the default reading functionality is
+to take the text of the “name” attribute of xml
and put it
+into the name
attribute of obj
. After that, we
+take the text of the “more” attribute of xml
and append it to
+the name
attribute of obj
. So the XML tree <foo name="Jim"
+more="XYZ"/>
is converted to a Foo
object
+with name
=“JimXYZ”.
In our :writer proc, we only take obj
(the Foo
+object to be written to XML) and xml
+(the XML tree the stuff is to be written to).
+Analogously to the :reader, we could take a proc that wraps the default
+writing functionality of the node, but we don't do that here–we
+completely override the writing functionality with our own code, which just
+takes the name
attribute of the object and writes “hi <the
+name> ho” to a bar
XML attribute in
+the XML tree (stupid example, I know).
As a special convention, if you specify both a :reader and a :writer for a
+node, and in both cases you do /not/ call the default behaviour, then you
+should use the generic node type node
, e.g.:
class SomeClass + include XML::Mapping + + .... + + node :reader=>proc{|obj,xml| ...}, + :writer=>proc{|obj,xml| ...} +end+ +
(since you're completely replacing both the reading and the writing +functionality, you're effectively replacing all the functionality of +the node, so it would be pointless and confusing to use one of the more +“specific” node types)
+ +As you see, the purpose of readers and writers is to make it possible to
+augment or override a node's functionality arbitrarily, so there
+shouldn't be anything that's absolutely impossible to achieve with
+xml-mapping. However, if you use readers and writers without invoking the
+default behaviour, you really do everything manually, so you're not
+doing any less work than you would do if you weren't using xml-mapping
+at all. So you'll probably use readers and/or writers for those bits of
+your mapping semantics that can't be achieved with xml-mapping's
+predefined node types (an alternative approach might be to override the
+post_load
and/or post_save
instance methods on
+the mapping class – see the reference documentation).
An advice similar to the one given above for marshallers/unmarshallers +applies here as well: If you find yourself writing lots of readers and +writers that only differ in some easily parameterizable aspects, you should +think about defining your own node types. We talk about that below, and it generally just means that you +move the (sensibly parameterized) code from your readers/writers to your +node types.
+ +Sometimes you might want to represent the same Ruby object in multiple +alternative ways in XML. For example, the name of a +“Person” object could be represented either in a “name” element or a “name” +attribute.
+ +xml-mapping supports this by allowing you to define multiple disjoint
+“mappings” for a mapping class. A mapping is by convention identified with
+a symbol, e.g. :my_mapping
, :other_mapping
etc.,
+and each mapping comprises a root element name and a set of node
+definitions. In the body of a mapping class definition, you switch to
+another mapping with use_mapping :the_mapping
. All following
+node declarations will be added to that mapping unless you specify
+the option :mapping=>:another_mapping for a node declaration (all node
+types support that option). The default mapping (the mapping used if there
+was no previous use_mapping
in the class body) is named
+:_default
.
All the worker methods like load_from_xml/file
,
+save_to_xml/file
, load_object_from_xml/file
+support a :mapping
keyword argument to specify the mapping,
+which again defaults to :_default
.
In the following example, we define two mappings (the default one and a
+mapping named :other
) for Person
objects with a
+name, an age and an address:
require 'xml/mapping' + +class Address; end + +class Person + include XML::Mapping + + # the default mapping. Stores the name and age in XML attributes, + # and the address in a sub-element "address". + + text_node :name, "@name" + numeric_node :age, "@age" + object_node :address, "address", :class=>Address + + use_mapping :other + + # the ":other" mapping. Non-default root element name; name and age + # stored in XML elements; address stored in the person's element + # itself + + root_element_name "individual" + text_node :name, "name" + numeric_node :age, "age" + object_node :address, ".", :class=>Address + + # you could also specify the mapping on a per-node basis with the + # :mapping option, e.g.: + # + # numeric_node :age, "age", :mapping=>:other +end + + +class Address + include XML::Mapping + + # the default mapping. + + text_node :street, "street" + numeric_node :number, "number" + text_node :city, "city" + numeric_node :zip, "zip" + + use_mapping :other + + # the ":other" mapping. + + text_node :street, "street-name" + numeric_node :number, "street-name/@number" + text_node :city, "city-name" + numeric_node :zip, "city-name/@zip-code" +end + + +### usage + +## XML representation of a person in the default mapping +xml = REXML::Document.new(' +<person name="Suzy" age="28"> + <address> + <street>Abbey Road</street> + <number>72</number> + <city>London</city> + <zip>18827</zip> + </address> +</person>').root + +## load using the default mapping +p = Person.load_from_xml xml +=> #<Person:0x007ff64a23e9c8 @name="Suzy", @age=28, @address=#<Address:0x007ff64a23d4b0 @street="Abbey Road", @number=72, @city="London", @zip=18827>> + +## save using the default mapping +xml2 = p.save_to_xml +xml2.write $stdout,2 +<person name='Suzy' age='28'> + <address> + <street> + Abbey Road + </street> + <number> + 72 + </number> + <city> + London + </city> + <zip> + 18827 + </zip> + </address> +</person> +## xml2 identical to xml + + +## now, save the same person to XML using the :other mapping... +other_xml = p.save_to_xml :mapping=>:other +other_xml.write $stdout,2 +<individual> + <name> + Suzy + </name> + <age> + 28 + </age> + <street-name number='72'> + Abbey Road + </street-name> + <city-name zip-code='18827'> + London + </city-name> +</individual> +## load it again using the :other mapping +p2 = Person.load_from_xml other_xml, :mapping=>:other +=> #<Person:0x007ff64a20c838 @name="Suzy", @age=28, @address=#<Address:0x007ff64a2079a0 @street="Abbey Road", @number=72, @city="London", @zip=18827>> + +## p2 identical to p+ +
In this example, each of the two mappings contains nodes that map the same +set of Ruby attributes (name, age and address). This is probably what you +want most of the time (since you're normally defining multiple XML mappings for the same Ruby data), but it's not +a necessity at all. When a mapping class is defined, xml-mapping will add +all Ruby attributes from all mappings to it.
+ +You may have noticed that the object_node
s in the
+Person
class apply the mapping they were themselves defined in
+to their sub-ordinated class (Address
). This is the case for
+all Single-attribute Nodes with Sub-objects
+(object_node
, array_node
and
+hash_node
) unless you explicitly specify a different mapping
+for the sub-object(s) using the option :sub_mapping, e.g.
object_node :address, "address", :class=>Address, :sub_mapping=>:other ++ +
It's easy to write additional node types and register them with the
+xml-mapping library (the following node types come with xml-mapping:
+node
, text_node
, numeric_node
,
+boolean_node
, object_node
,
+array_node
, hash_node
, choice_node
).
I'll first show an example, then some more theoretical insight.
+ +Let's say we want to extend the Signature
class from the
+example to include the time at which the signature was created. We want the
+new XML representation of such a signature to look
+like this:
<Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + <signed-on> + <day>13</day> + <month>2</month> + <year>2005</year> + </signed-on> +</Signature>+ +
(we only save year, month and day to make this example shorter), and the +mapping class declaration to look like this:
+ +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" + time_node :signed_on, "signed-on", :default_value=>Time.now +end ++ +
(i.e. a new “time_node” declaration was added).
+ +We want this time_node
call to define an attribute named
+signed_on
which holds the date value from the XML document in an instance of class Time
.
This node type can be defined with this piece of code:
+ +require 'xml/mapping/base' + +class TimeNode < XML::Mapping::SingleAttributeNode + def initialize(*args) + path,*args = super(*args) + @y_path = XML::XXPath.new(path+"/year") + @m_path = XML::XXPath.new(path+"/month") + @d_path = XML::XXPath.new(path+"/day") + args + end + + def extract_attr_value(xml) + y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i, + @m_path.first(xml).text.to_i, + @d_path.first(xml).text.to_i] + } + Time.local(y,m,d) + end + + def set_attr_value(xml, value) + @y_path.first(xml,:ensure_created=>true).text = value.year + @m_path.first(xml,:ensure_created=>true).text = value.month + @d_path.first(xml,:ensure_created=>true).text = value.day + end +end + + +XML::Mapping.add_node_class TimeNode ++ +
The last line registers the new node type with the xml-mapping library. The +name of the node factory method (“time_node”) is automatically derived from +the class name of the node type (“TimeNode”).
+ +There will be one instance of the node type TimeNode
per
+time_node
declaration per mapping class (not per mapping class
+instance). That instance (the “node” for short) will be created by the node
+factory method (time_node
); there's no need to instantiate
+the node type directly. The time_node
method places the node
+into the mapping class; the @owner attribute of the node is set to
+reference the mapping class. The node factory method passes the mapping
+class the node appears in (Signature
), followed by its own
+arguments, to the node's constructor. In the example, the
+time_node
method calls TimeNode.new(Signature,
+:signed_on, "signed-on", :default_value=>Time.now)
).
+new
of course creates the node and then delegates the
+arguments to our initializer initialize
. We first call the
+superclass's initializer, which strips off from the argument list those
+arguments it handles itself, and returns the remaining ones. In this case,
+the superclass XML::Mapping::SingleAttributeNode
+handles the Signature
, :signed_on
and
+:default_value=>Time.now
arguments – Signature
+is stored into @owner
, :signed_on
is stored into
+@attrname
, and {:default_value=>Time.now}
is
+stored into @options
. The remaining argument list
+["signed-on"]
is returned; we capture the
+"signed-on"
string in path (the rest of the
+argument list (an empty array) we capture in args for returning it
+at the end of the initializer. This isn't strictly necessary, it's
+just a convention that a node class initializer should always return those
+arguments it didn't handle itself). We'll interpret path
+as an XPath expression that locates the time value relative to the parent
+mapping object's XML tree (in this case, this
+would be the XML tree rooted at the
+<Signature>
element, i.e. the tree the
+Signature
instance was read from). We'll later have to
+read/store the year, month, and day values from
+path+"/year"
, path+"/month"
,
+and path+"/day"
, respectively, so we create (and
+precompile) three corresponding XPath expressions using XML::XXPath.new and store them into
+member variables of the node. XML::XXPath is
+an XPath implementation that is bundled with xml-mapping. It is very
+incomplete, but it supports writing (not just reading) of XML nodes, which is needed to support writing data back
+to XML. The XML::XXPath library is explained in more detail
+below.
The extract_attr_value
method is called whenever an instance
+of the mapping class the node belongs to (Signature
in the
+example) is being created from an XML tree. The
+parameter xml is that tree (again, this is the tree rooted at the
+<Signature>
element in this example). The method
+implementation is expected to extract the single attribute's value from
+xml and return it, or raise XML::Mapping::SingleAttributeNode::NoAttrValueSet
+if the attribute was “unset” in the XML (this
+exception tells the framework that the default value should be put in place
+if it was defined), or raise any other exception to signal an error and
+abort the whole process. Our superclass XML::Mapping::SingleAttributeNode
+will store the returned single attribute's value into the
+signed_on
attribute of the Signature
instance
+being read in. In our implementation, we apply the xpath expressions
+created during initialization to xml (e.g.
+@y_path.first(xml)
). An expression
+xpath_expr.first(xml) returns (as a REXML element) the first sub-element of xml
+that matches xpath_expr, or raises XML::XXPathError if there was no such
+element. We apply REXML's text method to the returned element
+to get out the element's text, convert it to integer, and supply it to
+the constructor of the Time
object to be returned. As a side
+note, if an XPath expression matches XML attributes,
+XML::XXPath methods like first will
+return XML::XXPath::Accessors::Attribute
+nodes that behave similarly to REXML::Element nodes, including support for
+messages like name and text, so this would've worked
+also if our XPath expressions had referred to XML
+attributes, not elements. The default_when_xpath_err
thing
+calls the supplied block and returns its value, but maps the exception XML::XXPathError to the mentioned XML::Mapping::SingleAttributeNode::NoAttrValueSet
+(any other exceptions fall through unchanged). As said above, XML::Mapping::SingleAttributeNode::NoAttrValueSet
+is caught by the framework (more precisely, by our superclass XML::Mapping::SingleAttributeNode),
+and the default value is set if it was provided. So you should just wrap
+default_when_xpath_err
around any applications of XPath
+expressions whose non-presence in the XML you want
+to be considered a non-presence of the attribute you're trying to
+extract. (XML::XXPath is designed to know knothing about XML::Mapping, so it doesn't raise XML::Mapping::SingleAttributeNode::NoAttrValueSet
+directly)
The set_attr_value
method is called whenever an instance of
+the mapping class the node belongs to (Signature
in the
+example) is being stored into an XML tree. The
+xml parameter is the XML tree (a REXML element node; here this is again the tree
+rooted at the <Signature>
element); value is
+the current value of the single attribute (in this example, the
+signed_on
attribute of the Signature
instance
+being stored). xml will most probably be “half-populated” by the
+time this method is called – the framework calls the
+set_attr_value
methods of all nodes of a mapping class in the
+order of their definition, letting each node fill its “bit” into
+xml. The method implementation is expected to write value
+into (the correct sub-elements of) xml, or raise an exception to
+signal an error and abort the whole process. No default value handling is
+done here; set_attr_value
won't be called at all if the
+attribute had been set to its default value. In our implementation we grab
+the year, month and day values from value (which must be a
+Time
), and store it into the sub-elements of xml
+identified by XPath expressions @y_path
, @m_path
+and @d_path
, respectively. We do this by calling XML::XXPath#first with an
+additional parameter :ensure_created=>true
. An expression
+xpath_expr.first(xml,:ensure_created=>true) works just
+like xpath_expr.first(xml) if xpath_expr was
+already present in xml. If it was not, it is created (preferably
+at the end of xml's list of sub-nodes), and returned. See below for a more detailed documentation of the XPath
+interpreter.
As just said, XML::XXPath, when used to
+create new XML nodes, generally appends those nodes
+to the end of the list of subnodes of the node the xpath expression was
+applied to. All xml-mapping nodes that come with xml-mapping use XML::XXPath when writing data to XML, and therefore also append their data to the XML data written by preceding nodes (the nodes are
+invoked in the order of their definition). This means that, generally, your
+output data will appear in the XML document in the
+same order in which the corresponding xml-mapping node definitions appeared
+in the mapping class (unless you used XPath expressions like foo which explicitly dictate a fixed position in the
+sequence of XML nodes). For instance, in the
+Order
class from the example at the beginning of this
+document, if we put the :signatures
node before the
+:items
node, the <Signed-By>
element will
+appear before the sequence of <Item>
elements
+in the output XML.
The following is a more systematic overview of the basic node types. The +description is self-contained, so some information from the previous +section will be repeated.
+ +A node type is implemented as a Ruby class derived from XML::Mapping::Node or one of its +subclasses.
+ +The following node types (node classes) come with xml-mapping (they all +live in the XML::Mapping namespace, which +I've left out here for brevity):
+ +Node + +-SingleAttributeNode + | +-SubObjectBaseNode + | | +-ObjectNode + | | +-ArrayNode + | | +-HashNode + | +-TextNode + | +-NumericNode + | +-BooleanNode + +-ChoiceNode+ +
XML::Mapping::Node is the base class
+for all nodes, XML::Mapping::SingleAttributeNode
+is the base class for single-attribute nodes,
+and XML::Mapping::SubObjectBaseNode
+is the base class for single-attribute nodes
+with sub-objects. XML::Mapping::TextNode, XML::Mapping::ArrayNode etc. are of
+course the text_node
, array_node
etc. we've
+talked about in this document. When you've written a new node class,
+you register it with xml-mapping by calling
+XML::Mapping.add_node_class MyNode
. When you do that,
+xml-mapping automatically defines the node factory method for your class –
+the method's name (e.g. my_node
) is derived from the
+node's class name (e.g. Foo::Bar::MyNode) by stripping all parent
+module names, and then converting capital letters to lowercase and
+preceding them with an underscore. In fact, this is just how all the
+predefined node types are defined – those node types are not “special”;
+they're defined in the source file
+xml/mapping/standard_nodes.rb
and then registered normally in
+xml/mapping.rb
. The source code of the built-in nodes is not
+very long or complicated; you may consider reading it in addition to this
+text to gain a better understanding.
The xml-mapping core “operates” node types as follows:
+ +As said above, when a node class is registered with xml-mapping by calling
+XML::Mapping.add_node_class TheNodeClass
, xml-mapping
+automatically generates the node factory method for that type. The node
+factory method will effectively be defined as a class method of the XML::Mapping module, which is why one can call
+it from the body of a mapping class definition. The generated method will
+create a new instance of the node class (a node for short) by
+calling new on the node class. The list of parameters to
+new will consist of the mapping class, followed by all
+arguments that were passed to the node factory method. For example,
+when you have this node declaration:
class MyMappingClass + include XML::Mapping + + my_node :foo, "bar", 42, :hi=>"ho" +end ++ +
, then the node factory method (my_node
) calls
+MyNode.new(MyMappingClass, :foo, "bar", 42,
+:hi=>"ho")
.
new of course creates the instance and calls initialize +on it. The initialize implementation will generally store the +parameters into some instance variables for later usage. As a convention, +initialize should always extract from the parameter list those +parameters it processes itself, process them, and return an array +containing the remaining (still unprocessed) parameters. Thus, an +implementation of initialize follows this pattern:
+ +def initialize(*args) + myparam1,myparam2,...,myparamx,*args = super(*args) + + .... process the myparam1,myparam2,...,myparamx .... + + # return still unprocessed args + args +end+ +
(since the called superclass initializer is written the same way, the +parameter array returned by it will already be stripped of all parameters +that the superclass initializer (or any of its superclasses's +initializers) processed)
+ +This technique is a simple way to “chain” the initializers of all +superclasses of a node class, starting with the topmost one (Node), so that +each initializer can easily find out and process the parameters it is +responsible for.
+ +The base node class XML::Mapping::Node +provides an initialize implementation that, among other things +(described below), adds self (i.e. the created node) to the +internal list of nodes held by the mapping class, and sets the @owner +attribute of self to reference the mapping class.
+ +So, effectively there will be one instance of a node class (a node) per +node definition, and that instance lives in the mapping class the node was +defined in.
+ +When an instance of a mapping class is created or filled from an XML tree, xml-mapping will call xml_to_obj
+on all nodes defined in that mapping class in the mapping the node is defined in, in the order of
+their definition. Two parameters will be passed: the mapping class instance
+being created/filled, and the XML tree the instance
+is being created/filled from. The implementation of xml_to_obj
+is expected to read whatever pieces of data it is responsible for from the
+XML tree and put it into the appropriate
+variables/attributes etc. of the instance.
When an instance of a mapping class is stored or filled into an XML tree, xml-mapping will call obj_to_xml
+on all nodes defined in that mapping class in the mapping the node is defined in, in the order of
+their definition, again passing as parameters the mapping class instance
+being stored, and the XML tree the instance is being
+stored/filled into. The implementation of obj_to_xml
is
+expected to read whatever pieces of data it is responsible for from the
+instance and put it into the appropriate XML
+elements/XML attr etc. of the XML tree.
The following is an overview of how initialization and +marshalling/unmarshalling is implemented in the node base classes (Node, +SingleAttributeNode, and SubObjectBaseNode).
+ +TODO: summary table: member var name; introduced in class; meaning
+ +In initialize, the mapping class and the option arguments are
+stripped from the argument list. The mapping class is stored in @owner, the
+option arguments are stored (as a hash) in @options (the hash will be empty
+if no options were given). The mapping the node
+is defined in is determined (:mapping option, last use_mapping
+or :_default
) and stored in @mapping. The node then stores
+itself in the list of nodes of the mapping class belonging to the mapping
+(@owner.xml_mapping_nodes(:mapping=>@mapping)
; see XML::Mapping::ClassMethods#xml_mapping_nodes).
+This list is the list of nodes later used when marshalling/unmarshalling an
+instance of the mapping class with respect to a given mapping. This means
+that node implementors will not normally “see” anything of the mapping
+(they don't need to access the @mapping variable) because the
+marshalling/unmarshalling methods
+(obj_to_xml
/xml_to_obj
) simply won't be
+called if the node's mapping is not the same as the mapping the
+marshalling/unmarshalling is happening with.
Furthermore, if :reader and/or :writer options were given,
+xml_to_obj
resp. obj_to_xml
are transparently
+overwritten on the node to delegate to the supplied :reader/:writer procs.
The marshalling/unmarshalling methods
+(obj_to_xml
/xml_to_obj
) are not implemented in
+Node
(they just raise an exception).
In initialize, the attribute name is stripped from the argument +list and stored in @attrname, and an attribute of that name is added to the +mapping class the node belongs to.
+ +During marshalling/unmarshalling of an object to/from XML, single-attribute nodes only read/write a single
+piece of the object's state: the single attribute (@attrname) the node
+handles. Because of this, the
+obj_to_xml
/xml_to_obj
implementations in
+SingleAttributeNode call two new methods introduced by SingleAttributeNode,
+which must be overwritten by subclasses:
extract_attr_value(xml) + +set_attr_value(xml, value) ++ +
extract_attr_value(xml)
is called by xml_to_obj
+during unmarshalling. xml is the XML tree
+being read. The method must read the attribute's value from
+xml and return it. xml_to_obj
will set the attribute
+to that value.
set_attr_value(xml, value)
is called by
+obj_to_xml
during marshalling. xml is the XML tree being written, value is the current
+value of the attribute. The method must write value into (the
+correct sub-elements/attributes) of xml.
SingleAttributeNode also handles the default value, if it was specified
+(via the :default_value option): When writing data to XML, set_attr_value(xml, value)
won't
+be called if the attribute was set to the default value. When reading data
+from XML, the extract_attr_value(xml)
+implementation must raise a special exception, XML::Mapping::SingleAttributeNode::NoAttrValueSet,
+if it wants to indicate that the data was not present in the XML. SingleAttributeNode will catch this exception and
+put the default value, if it was defined, into the attribute.
The initializer will set up additional member variables @sub_mapping, +@marshaller, and @unmarshaller.
+ +@sub_mapping contains the mapping to be used when reading/writing the +sub-objects (either specified with :sub_mapping, or, by default, the +mapping the node itself was defined in).
+ +@marshaller and @unmarshaller contain procs that encapsulate
+writing/reading of sub-objects to/from XML, as
+specified by the user with :class/:marshaller/:unmarshaller etc. options
+(the meaning of those different options was described above). The procs are there to be called from
+extract_attr_value
or set_attr_value
whenever the
+need arises.
XML::XXPath is an XPath parser. It is used in
+xml-mapping node type definitions, but can just as well be utilized
+stand-alone (it does not depend on xml-mapping). XML::XXPath is very incomplete and probably will
+always be, but it should be reasonably efficient (XPath expressions are
+precompiled), and, most importantly, it supports write access, which is
+needed for writing objects to XML. For example, if
+you create the path /foo/bar[3]/baz[@key='hiho']
in the XML document
<foo> + <bar> + <baz key="ab">hello</baz> + <baz key="xy">goodbye</baz> + </bar> +</foo>+ +
, you'll get:
+ +<foo> + <bar> + <baz key='ab'>hello</baz> + <baz key='xy'>goodbye</baz> + </bar> + <bar/> + <bar> + <baz key='hiho'/> + </bar> +</foo>+ +
XML::XXPath is explained in more detail in +the reference documentation and the user_manual_xxpath file.
+ +xml-mapping is licensed under the Apache License, version 2.0. See the +LICENSE file for details.
+Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.
+ +For downloading the latest version, git repository access etc. go to:
+ +github.com/multi-io/xml-mapping
+ +(example document stolen + extended from www.castor.org/xml-mapping.html)
+ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<Order reference="12343-AHSHE-314159"> + <Client> + <Name>Jean Smith</Name> + <Address where="home"> + <City>San Mateo</City> + <State>CA</State> + <ZIP>94403</ZIP> + <Street>2000, Alameda de las Pulgas</Street> + </Address> + <Address where="work"> + <City>San Francisco</City> + <State>CA</State> + <ZIP>94102</ZIP> + <Street>98765, Fulton Street</Street> + </Address> + </Client> + + <Item reference="RF-0001"> + <Description>Stuffed Penguin</Description> + <Quantity>10</Quantity> + <UnitPrice>8.95</UnitPrice> + </Item> + + <Item reference="RF-0034"> + <Description>Chocolate</Description> + <Quantity>5</Quantity> + <UnitPrice>28.50</UnitPrice> + </Item> + + <Item reference="RF-3341"> + <Description>Cookie</Description> + <Quantity>30</Quantity> + <UnitPrice>0.85</UnitPrice> + </Item> + + <Signed-By> + <Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + </Signature> + + <Signature> + <Name>Jill Smith</Name> + <Position>clerk</Position> + </Signature> + + <Signature> + <Name>Miles O'Brien</Name> + </Signature> + </Signed-By> + +</Order>+ +
require 'xml/mapping' + +## forward declarations +class Client; end +class Address; end +class Item; end +class Signature; end + + +class Order + include XML::Mapping + + text_node :reference, "@reference" + object_node :client, "Client", :class=>Client + hash_node :items, "Item", "@reference", :class=>Item + array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] + + def total_price + items.values.map{|i| i.total_price}.inject(0){|x,y|x+y} + end +end + + +class Client + include XML::Mapping + + text_node :name, "Name" + object_node :home_address, "Address[@where='home']", :class=>Address + object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil +end + + +class Address + include XML::Mapping + + text_node :city, "City" + text_node :state, "State" + numeric_node :zip, "ZIP" + text_node :street, "Street" +end + + +class Item + include XML::Mapping + + text_node :descr, "Description" + numeric_node :quantity, "Quantity" + numeric_node :unit_price, "UnitPrice" + + def total_price + quantity*unit_price + end +end + + +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" +end ++ +
####read access +o=Order.load_from_file("order.xml") +=> #<Order:0x007ff64a0fe8b0 @signatures=[#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">], @reference="12343-AHSHE-314159", @client=#<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>, @items={"RF-0001"=>#<Item:0x007ff64a0df550 @descr="Stuffed Penguin", @quantity=10, @unit_price=8.95>, "RF-0034"=>#<Item:0x007ff64a0ddbd8 @descr="Chocolate", @quantity=5, @unit_price=28.5>, "RF-3341"=>#<Item:0x007ff64a0dc0d0 @descr="Cookie", @quantity=30, @unit_price=0.85>}> +o.reference +=> "12343-AHSHE-314159" +o.client +=> #<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">> +o.items.keys +=> ["RF-0001", "RF-0034", "RF-3341"] +o.items["RF-0034"].descr +=> "Chocolate" +o.items["RF-0034"].total_price +=> 142.5 +o.signatures +=> [#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">] +o.signatures[2].name +=> "Miles O'Brien" +o.signatures[2].position +=> "Some Employee" +## default value was set + +o.total_price +=> 257.5 + +####write access +o.client.name="James T. Kirk" +o.items['RF-4711'] = Item.new +o.items['RF-4711'].descr = 'power transfer grid' +o.items['RF-4711'].quantity = 2 +o.items['RF-4711'].unit_price = 29.95 + +s=Signature.new +s.name='Harry Smith' +s.position='general manager' +o.signatures << s +xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) +=> <order reference='12343-AHSHE-314159'> ... </> +xml.write($stdout,2) +<order reference='12343-AHSHE-314159'> + <Client> + <Name> + James T. Kirk + </Name> + <Address where='home'> + <City> + San Mateo + </City> + <State> + CA + </State> + <ZIP> + 94403 + </ZIP> + <Street> + 2000, Alameda de las Pulgas + </Street> + </Address> + <Address where='work'> + <City> + San Francisco + </City> + <State> + CA + </State> + <ZIP> + 94102 + </ZIP> + <Street> + 98765, Fulton Street + </Street> + </Address> + </Client> + <Item reference='RF-0001'> + <Description> + Stuffed Penguin + </Description> + <Quantity> + 10 + </Quantity> + <UnitPrice> + 8.95 + </UnitPrice> + </Item> + <Item reference='RF-0034'> + <Description> + Chocolate + </Description> + <Quantity> + 5 + </Quantity> + <UnitPrice> + 28.5 + </UnitPrice> + </Item> + <Item reference='RF-3341'> + <Description> + Cookie + </Description> + <Quantity> + 30 + </Quantity> + <UnitPrice> + 0.85 + </UnitPrice> + </Item> + <Item reference='RF-4711'> + <Description> + power transfer grid + </Description> + <Quantity> + 2 + </Quantity> + <UnitPrice> + 29.95 + </UnitPrice> + </Item> + <Signed-By> + <Signature> + <Name> + John Doe + </Name> + <Position> + product manager + </Position> + </Signature> + <Signature> + <Name> + Jill Smith + </Name> + <Position> + clerk + </Position> + </Signature> + <Signature> + <Name> + Miles O'Brien + </Name> + </Signature> + <Signature> + <Name> + Harry Smith + </Name> + <Position> + general manager + </Position> + </Signature> + </Signed-By> +</order> + + +####Starting a new order from scratch +o = Order.new +=> #<Order:0x007ff64a206050 @signatures=[]> +## attributes with default values (here: signatures) are set +## automatically + +xml=o.save_to_xml +XML::MappingError: no value, and no default value, for attribute: reference + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:724:in `obj_to_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:218:in `block in fill_into_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `each' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `fill_into_xml' + from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:229:in `save_to_xml' +## can't save as long as there are still unset attributes without +## default values + +o.reference = "FOOBAR-1234" + +o.client = Client.new +o.client.name = 'Ford Prefect' +o.client.home_address = Address.new +o.client.home_address.street = '42 Park Av.' +o.client.home_address.city = 'small planet' +o.client.home_address.zip = 17263 +o.client.home_address.state = 'Betelgeuse system' + +o.items={'XY-42' => Item.new} +o.items['XY-42'].descr = 'improbability drive' +o.items['XY-42'].quantity = 3 +o.items['XY-42'].unit_price = 299.95 + +xml=o.save_to_xml +xml.write($stdout,2) + +<order reference='FOOBAR-1234'> + <Client> + <Name> + Ford Prefect + </Name> + <Address where='home'> + <City> + small planet + </City> + <State> + Betelgeuse system + </State> + <ZIP> + 17263 + </ZIP> + <Street> + 42 Park Av. + </Street> + </Address> + </Client> + <Item reference='XY-42'> + <Description> + improbability drive + </Description> + <Quantity> + 3 + </Quantity> + <UnitPrice> + 299.95 + </UnitPrice> + </Item> +</order> +## the root element name when saving an object to XML will by default +## be derived from the class name (in this example, "Order" became +## "order"). This can be overridden on a per-class basis; see +## XML::Mapping::ClassMethods#root_element_name for details.+ +
As shown in the example, you have to include XML::Mapping into a class to turn it into a +“mapping class”. There are no other restrictions imposed on mapping +classes; you can add attributes and methods to them, include additional +modules in them, derive them from other classes, derive other classes from +them etc.pp.
+ +An instance of a mapping class can be created from/converted into an XML node with methods like XML::Mapping::ClassMethods#load_from_xml, +XML::Mapping#save_to_xml, +XML::Mapping::ClassMethods#load_from_file, +XML::Mapping#save_to_file. +Special class methods like “text_node”, “array_node” etc., called +node factory methods, may be called from the +body of the class definition to define instance attributes that are +automatically and bidirectionally mapped to subtrees of the XML element an instance of the class is mapped to.
+ +For example, in the definition
+ +class Address + include XML::Mapping + + text_node :city, "City" + text_node :state, "State" + numeric_node :zip, "ZIP" + text_node :street, "Street" +end ++ +
the first call to text_node creates an attribute named “city” which is
+mapped to the text of the XML child element defined
+by the XPath expression “City” (xml-mapping includes an XPath interpreter
+that can also be used seperately; see below). When
+you create an instance of Address
from an XML element (using Address.load_from_file(file_name) or
+Address.load_from_xml(rexml_element)), that instance's “city” attribute
+will be set to the text of the XML element's
+“City” child element. When you convert an instance of Address
+into an XML element, a sub-element “City” is added
+and its text is set to the current value of the city
+attribute. The other node types (numeric_node, array_node etc.) work
+analogously. Generally said, when an instance of the above
+Address
class is created from or converted to an XML tree, each of the four nodes in the class maps some
+parts of that XML tree to a single, specific
+attribute of the Adress
instance. The name of that attribute
+is given in the first argument to the node factory method. Such a node is
+called a “single-attribute node”. All node types that come with xml-mapping
+except one (choice_node
, which I'll talk about below) are
+single-attribute nodes.
For each single-attribute node you may define a default value +which will be set if there was no value defined for the attribute in the XML source.
+ +From the example:
+ +class Signature + include XML::Mapping + + text_node :position, "Position", :default_value=>"Some Employee" +end ++ +
The semantics of default values are as follows:
+when creating a new instance from scratch:
+attributes with default values are set to their default values
+attributes without default values are left unset
+(when defining your own initializer, you'll have to call the inherited +initialize method in order to get this behaviour)
+when loading an instance from an XML document:
+ +when saving an instance to an XML document:
+unset attributes without default values raise an error
+attributes with default values that are set to their default values are +not saved
+all other attributes are saved
+This implies that:
+attributes that are set to their respective default values are not +represented in the XML
+attributes without default values must be set explicitly before saving
+Single-attribute nodes of type array_node
,
+hash_node
, and object_node
recursively map one or
+more subtrees of their XML to sub-objects (e.g.
+array elements or hash values) of their attribute. For example, with the
+line
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] ++ +
, an attribute named “signatures” is added to the surrounding class (here:
+Order
); the attribute will be an array whose elements
+correspond to the XML sub-trees yielded by the XPath
+expression “Signed-By/Signature” (relative to the tree corresponding to the
+Order
instance). Each element will be of class
+Signature
(internally, each element is created from its
+corresponding XML subtree by just calling
+Signature.load_from_xml(the_subtree)
). The reason why the path
+“Signed-By/Signature” is provided in two arguments instead of just one
+combined one becomes apparent when marshalling the array (along with the
+surrounding Order
object) back into a sequence of XML elements. When that happens, “Signed-By” names the
+common base element for all those elements, and “Signature” is the path
+that will be duplicated for each element. For example, when the
+signatures
attribute contains an array with 3
+Signature
instances (let's call them sig1
,
+sig2
, and sig3
) in it, it will be marshalled to
+an XML tree that looks like this:
<Signed-By> + <Signature> + [marshalled object sig1] + </Signature> + <Signature> + [marshalled object sig2] + </Signature> + <Signature> + [marshalled object sig3] + </Signature> +</Signed-By>+ +
Internally, each Signature
instance is stored into its
+<Signature>
sub-element by calling
+the_signature_instance.fill_into_xml(the_sub_element)
. The
+input document in the example above shows how this ends up looking.
hash_node
s work similarly, but they define hash-valued
+attributes instead of array-valued ones.
object_node
s are the simplest of the three types of
+single-attribute nodes with sub-objects. They just map a single given
+subtree directly to their attribute value. See the example for examples :)
The mentioned methods load_from_xml
and
+fill_into_xml
are the only methods classes must implement in
+order to be usable in the :class=>
keyword arguments to
+node factory methods. Mapping classes (i.e. classes that include
+XML::Mapping
) automatically inherit those functions and can thus be
+readily used in :class=>
arguments, as shown for the
+Signature
class in the array_node
call above. In
+addition to that, xml-mapping adds those methods to some of Ruby's core
+classes, namely String
and Numeric
(and thus
+Float
, Integer
, and BigInt
). So you
+can also use strings or numbers as sub-objects of attributes of
+array_node
, hash_node
, or
+object_node
nodes. For example, say you have an XML document like this one:
<?xml version="1.0" encoding="ISO-8859-1"?> + +<people> + <names> + <name>Jim</name> + <name>Susan</name> + <name>Herbie</name> + <name>Nancy</name> + </names> +</people>+ +
, and you want to map all the names to a string array attribute
+names
, you could do it like this:
require 'xml/mapping' +class People + include XML::Mapping + array_node :names, "names", "name", :class=>String +end ++ +
usage:
+ +ppl=People.load_from_file("stringarray.xml") +=> #<People:0x007ff64a0cda08 @names=["Jim", "Susan", "Herbie", "Nancy"]> +ppl.names +=> ["Jim", "Susan", "Herbie", "Nancy"] + +ppl.names.concat ["Mary","Arnold"] +=> ["Jim", "Susan", "Herbie", "Nancy", "Mary", "Arnold"] +ppl.save_to_xml.write $stdout,2 + +<people> + <names> + <name> + Jim + </name> + <name> + Susan + </name> + <name> + Herbie + </name> + <name> + Nancy + </name> + <name> + Mary + </name> + <name> + Arnold + </name> + </names> +</people>+ +
As a side node, this feature actually makes text_node
and
+numeric_node
special cases of object_node
. For
+example, text_node :attr, "path"
is the same as
+object_node :attr, "path", :class=>String
.
Besides the :class
keyword argument, there are alternative
+ways for a single-attribute node with sub-objects to specify the way the
+sub-objects are created from/marshalled into their subtrees.
First, it's possible not to specify anything at all – in that case, the +class of a sub-object will be automatically deduced from the root element +name of its subtree. This allows you to achieve a kind of “polymorphic”, +late-bound way to decide about the sub-object's class. The following +example document contains a hierarchical, recursive set of named +“documents” and “folders”, where folders hold a set of entries, each of +which may again be either a document or a folder:
+ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<folder name="home"> + <document name="plan"> + <contents> inhale, exhale</contents> + </document> + + <folder name="work"> + <folder name="xml-mapping"> + <document name="README"> + <contents>foo bar baz</contents> + </document> + </folder> + </folder> + +</folder>+ +
This can be mapped to Ruby like this:
+ +require 'xml/mapping' + +class Entry + include XML::Mapping + + text_node :name, "@name" +end + + +class Document <Entry + include XML::Mapping + + text_node :contents, "contents" +end + + +class Folder <Entry + include XML::Mapping + + array_node :entries, "document|folder", :default_value=>[] + + def [](name) + entries.select{|e|e.name==name}[0] + end + + def append(name,entry) + entries << entry + entry.name = name + entry + end +end ++ +
Usage:
+ +root = XML::Mapping.load_object_from_file "documents_folders.xml" +=> #<Folder:0x007ff6499c0f58 @entries=[#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">], @name="home"> +root.name +=> "home" +root.entries +=> [#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">] + +root.append "etc", Folder.new +root["etc"].append "passwd", Document.new +root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh" +root["etc"].append "hosts", Document.new +root["etc"]["hosts"].contents = "127.0.0.1 localhost" + +xml = root.save_to_xml +=> <folder name='home'> ... </> +xml.write $stdout,2 + +<folder name='home'> + <document name='plan'> + <contents> + inhale, exhale + </contents> + </document> + <folder name='work'> + <folder name='xml-mapping'> + <document name='README'> + <contents> + foo bar baz + </contents> + </document> + </folder> + </folder> + <folder name='etc'> + <document name='passwd'> + <contents> + foo:x:2:2:/bin/sh + </contents> + </document> + <document name='hosts'> + <contents> + 127.0.0.1 localhost + </contents> + </document> + </folder> +</folder>+ +
As you see, the Folder#entries
attribute is mapped via an
+array_node that does not specify a :class
or anything else to
+govern the instantiation of the array's elements. This causes
+xml-mapping to deduce the class of each array element from the root element
+name of the corresponding XML tree. In this example,
+the root element name is either “document” or “folder”. The mapping between
+root element names and class names is the one briefly described in example at the beginning of this document – the
+unqualified class name is just converted to lower case and “dashed”, e.g.
+Foo::Bar::MyClass becomes “my-class”; and you may overwrite this on a
+per-class basis by calling root_element_name
+"the-new-name"
in the class body. In our example, the root
+element name “document” leads to an instantiation of class
+Document
, and the root element name “folder” leads to an
+instantiation of class Folder
.
Incidentally, the last example shows that you can readily derive mapping
+classes from one another (as said before, you can also derive mapping
+classes from other classes, include other modules into them etc. at will).
+This works just like intuition thinks it should – when deriving one mapping
+class from another one, the list of nodes in effect when loading/saving
+instances of the derived class will consist of all nodes of that class and
+all superclasses, starting with the topmost superclass that has nodes
+defined. There is one thing to take care of though: When deriving mapping
+classes from one another, you have to make sure to include
+XML::Mapping
in each class. This requirement exists purely due to
+ease-of-implementation considerations; there are probably ways to do away
+with it, but the inconvenience seemed not severe enough for me to bother
+(as yet). Still, you might get “strange” errors if you forget to do it for
+a class.
Besides the :class
keyword argument and no argument, there is
+a third way to specify the way the sub-objects are created from/marshalled
+into their subtrees: :marshaller
and/or
+:unmarshaller
keyword arguments. Here you pass procs in which
+you just do all the work manually. So this is basically a “catch-all” for
+cases where the other two alternatives are not appropriate for the problem
+at hand. (TODO: Use other example?) Let's say we want to
+extend the Signature
class from the initial example to include
+the date on which the signature was created. We want the new XML representation of such a signature to look like
+this:
<Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + <signed-on> + <day>13</day> + <month>2</month> + <year>2005</year> + </signed-on> +</Signature>+ +
So, a new “signed-on” element was added that holds the day, month, and
+year. In the Signature
instance in Ruby, we want the date to
+be stored in an attribute named signed_on
of type
+Time
(that's Ruby's built-in Time
class).
One could think of using object_node
, but something like
+object_node :signed_on, "signed-on", :class=>Time
+won't work because Time
isn't a mapping class and
+doesn't define methods load_from_xml
and
+fill_into_xml
(we could easily define those though; we'll
+talk about that possibility here and here). The fastest, most ad-hoc way to
+achieve what we want are :marshaller and :unmarshaller keyword arguments,
+like this:
require 'xml/mapping' +require 'xml/xxpath_methods' + +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" + object_node :signed_on, "signed-on", + :unmarshaller=>proc{|xml| + y,m,d = [xml.first_xpath("year").text.to_i, + xml.first_xpath("month").text.to_i, + xml.first_xpath("day").text.to_i] + Time.local(y,m,d) + }, + :marshaller=>proc{|xml,value| + e = xml.elements.add; e.name = "year"; e.text = value.year + e = xml.elements.add; e.name = "month"; e.text = value.month + e = xml.elements.add; e.name = "day"; e.text = value.day + + # xml.first("year",:ensure_created=>true).text = value.year + # xml.first("month",:ensure_created=>true).text = value.month + # xml.first("day",:ensure_created=>true).text = value.day + } +end ++ +
The :unmarshaller
proc will be called whenever a
+Signature
instance is being read in from an XML source. The xml
argument passed to the
+proc contains (as a REXML::Element
+instance) the XML subtree corresponding to the
+node's attribute's sub-object currently being read. In the case of
+our object_node
, the sub-object is just the node's
+attribute (signed_on
) itself, and the subtree is the one
+rooted at the <signed-on> element (if this were e.g. an
+array_node
, the :unmarshaller
proc would be
+called once for each array element, and xml
would hold the
+subtree corresponding to the “current” array element). The proc is expected
+to extract the sub-object's data from xml
and return the
+sub-object. So we have to read the “year”, “month”, and “day” elements,
+construct a Time
instance from them and return that. One could
+just use the REXML API to do that, but I've
+decided here to use the XPath interpreter that comes with xml-mapping
+(xml/xxpath), and specifically the 'xml/xxpath_methods' utility
+library that adds methods like first
to REMXML::Element. We
+call first
on xml
three times, passing XPath
+expressions to extract the “year”/“month”/“day” sub-elements, construct the
+Time
instance from that and return it. The XPath library is
+explained in more detail below.
The :marshaller
proc will be called whenever a
+Signature
instance is being written into an XML tree. xml
is again the XML subtree rooted at the <signed-on> element (it
+will still be empty when this proc is called), and value
is
+the current value of the sub-object (again, since this is an
+object_node
, value
is the node's attribute,
+i.e. the Time
instance). We have to fill xml
with
+the data from value
here. So we add three elements “year”,
+“month” and “day” and set their texts to the corresponding values from
+value
. The commented-out code shows an alternative
+implementation of the same thing using the XPath interpreter.
It should be mentioned again that :marshaller/:unmarshaller procs are
+possible with all single-attribute nodes with sub-objects, i.e. with
+object_node
, array_node
, and
+hash_node
. So, if you wanted to map a whole array of date
+values, you could use array_node
with the same
+:marshaller/:unmarshaller procs as above, for example:
array_node :birthdays, "birthdays", "birthday", + :unmarshaller=> <as above>, + :marshaller=> <as above>+ +
You can see that :marshaller/:unmarshaller procs give you more flexibility,
+but they also impose more work because you essentially have to do all the
+work of marshalling/unmarshalling the sub-objects yourself. If you find
+yourself copying and pasting marshaller/unmarshaller procs all over the
+place, you should instead define your own node type or mix the
+marshalling/unmarshalling capabilities into the Time
class
+itself. This is explained here and here, and you'll see that it's not
+really much more work than writing :marshaller and :unmarshaller procs (you
+essentially just move the code from those procs into your own node type
+resp. into the Time
class), so you should not hesitate to do
+this.
Another thing worth mentioning is that you don't have to specify
+both a :marshaller and an :unmarshaller simultaneously. You can as
+well give only one of them, and in addition to that pass a
+:class
argument or no argument. When you do that, the
+specified marshaller (or unmarshaller) will be used when marshalling (resp.
+unmarshalling) the sub-objects, and the other passed argument
+(:class
or none) will be employed when unmarshalling (resp.
+marshalling) the sub-objects. So, in effect, you can deactivate or
+“short-cut” some part of the marshalling/unmarshalling functionality of a
+node type while retaining another part.
I'll shed some more light on how single-attribute nodes add mapped +attributes to Ruby classes. An attribute declaration like
+ +text_node :city, "City" ++ +
maps some portion of the XML tree (here: the “City”
+sub-element) to an attribute (here: “city”) of the class whose body the
+declaration appears in. When writing (marshalling) instances of the
+surrounding class into an XML document, xml-mapping
+will read the attribute value from the instance using the function named
+city
; when reading (unmarshalling) an instance from an XML document, xml-mapping will use the one-parameter
+function city=
to set the attribute in the instance to the
+value read from the XML document.
If these functions don't exist at the time the node declaration is
+executed, xml-mapping adds default implementations that simply read/write
+the attribute value to instance variables that have the same name as the
+attribute. For example, the city
attribute declaration in the
+Address
class in the example added functions city
+and city=
that read/write from/to the instance variable
+@city
.
If, however, these functions already exist prior to defining the +attributes, xml-mapping will leave them untouched, so your precious +self-written accessor methods that do whatever complicated internal +processing of the data won't be overwritten.
+ +This means that you can not only create new mapping classes from scratch, +you can also take existing classes that contain some “business logic” and +“augment” them with xml-mapping capabilities. As a simple example, +let's augment Ruby's “Time” class with node declarations that +declare XML mappings for the day, month etc. fields:
+ +class Time + include XML::Mapping + + numeric_node :year, "year" + numeric_node :month, "month" + numeric_node :day, "mday" + numeric_node :hour, "hours" + numeric_node :min, "minutes" + numeric_node :sec, "seconds" +end + + +nowxml=Time.now.save_to_xml +=> <time> ... </> +nowxml.write($stdout,2) +<time> + <year> + 2015 + </year> + <month> + 3 + </month> + <mday> + 1 + </mday> + <hours> + 15 + </hours> + <minutes> + 31 + </minutes> + <seconds> + 6 + </seconds> +</time>+ +
Here XML mappings are defined for the existing
+fields year
, month
etc. Xml-mapping noticed that
+the getter methods for those attributes existed, so it didn't overwrite
+them. When calling save_to_xml
on a Time
object,
+these methods are called and return the object's values for those
+fields, which then get written to the output XML.
So you can convert Time
objects into XML trees. What about reading them back in from XML? All XML reading operations
+go through <Class>.load_from_xml
. The
+load_from_xml
class method inherited from XML::Mapping (see XML::Mapping::ClassMethods#load_from_xml)
+allocates a new instance of the class (Time
), then calls
+fill_from_xml
(i.e. XML::Mapping#fill_from_xml)
+on it. fill_from_xml
iterates over all our nodes in the order
+of their definition. For each node, its data (the <year>, or
+<month>, or <day> etc. element) is read from the XML source and then written to the Time
+instance via the respective setter method (year=
,
+month=
, day=
etc.). These methods didn't
+exist in Time
before (Time
objects are
+immutable), so xml-mapping defined its own, default setter methods that
+just set @year
, @month
etc. This is of course
+pretty useless because Time
objects don't hold their time
+in these variables, so the setter methods don't really change the time
+of the Time
object. So we have to redefine
+load_from_xml
for the Time
class:
def Time.load_from_xml(xml, options={:mapping=>:_default}) + year,month,day,hour,min,sec = + [xml.first_xpath("year").text.to_i, + xml.first_xpath("month").text.to_i, + xml.first_xpath("mday").text.to_i, + xml.first_xpath("hours").text.to_i, + xml.first_xpath("minutes").text.to_i, + xml.first_xpath("seconds").text.to_i] + Time.local(year,month,day,hour,min,sec) +end ++ +
All nodes I've shown so far (node types text_node, numeric_node, +boolean_node, object_node, array_node, and hash_node) were single-attribute +nodes: The first parameter to the node factory method of such a node is an +attribute name, and the attribute of that name is the only piece of the +state of instances of the node's mapping class that gets read/written +by the node.
+ +There is one node type distributed with xml-mapping that is not a
+single-attribute node: choice_node
. A choice_node
+allows you to specify a sequence of pairs, each consisting of an XPath
+expression and another node (any node is supported here, including other
+choice_nodes). When reading in an XML source, the
+choice_node will delegate the work to the first node in the sequence whose
+corresponding XPath expression was matched in the XML. When writing an object back to XML, the choice_node will delegate the work to the
+first node whose data was “present” in the object (for single-attribute
+nodes, the data is considered “present” if the node's attribute is
+non-nil; for choice_nodes, the data is considered “present” if at least one
+of the node's sub-nodes is “present”).
As a (somewhat contrived) example, here's a mapping for
+Publication
objects that have either a single author
+(contained in an “author” XML attribute) or several
+“contributors” (contained in a sequence of “contr” XML elements):
class Publication + include XML::Mapping + + choice_node :if, '@author', :then, (text_node :author, '@author'), + :elsif, 'contr', :then, (array_node :contributors, 'contr', :class=>String) +end + +### usage + +p1 = Publication.load_from_xml(REXML::Document.new('<publication author="Jim"/>').root) +=> #<Publication:0x007ff64a166a78 @author="Jim"> + +p2 = Publication.load_from_xml(REXML::Document.new(' +<publication> + <contr>Chris</contr> + <contr>Mel</contr> + <contr>Toby</contr> +</publication>').root) +=> #<Publication:0x007ff64a155f48 @contributors=["Chris", "Mel", "Toby"]>+ +
The symbols :if, :then, and :elsif (but not :else – see below) in the
+choice_node
's node factory method call are ignored; they
+may be sprinkled across the argument list at will (preferably the way shown
+above of course) to increase readability.
The rest of the arguments specify the mentioned sequence of XPath +expressions and corresponding nodes.
+ +When reading a Publication
object from XML, the XPath expressions from the
+choice_node
(@author
and contr
) will
+be matched in sequence against the source XML tree
+until a match is found or the end of the argument list is reached. If the
+end is reached, an exception is raised. Otherwise, for the first XPath
+expression that matched, the corresponding node will be invoked (i.e. used
+to read actual data from the XML source into the
+Person
object). If you specify :else, :default, or :otherwise
+in place of an XPath expression, this is treated as an XPath expression
+that always matches. So you can use :else (or :default or :otherwise) for a
+“fallback” node that will be used if none of the other XPath expressions
+matched (an example for this follows).
When writing a Publication
object back to XML, the first node in the sequence whose data is
+“present” in the source object will be invoked to write data from the
+object into the target XML tree (and the
+corresponding XPath expression will be created in the XML tree if it doesn't exist already). If there is
+no such node in the sequence, an exception is raised. As said above, for
+single-attribute nodes, the node's data is considered “present” if the
+node's attribute is non-nil. So, if you write a
+Publication
object to XML, and either
+the author
or the contributors
attribute of the
+object is set, it will be written; if both attributes are nil, an exception
+will be raised.
A frequent use case for choice_nodes will probably be object attributes
+that may be represented in multiple alternative ways in XML. As an example, consider “Person” objects where the
+name of the person should be stored alternatively in a sub-element named
+name
, or an attribute named name
, or in the text
+of the person
element itself. You can achieve this with
+choice_node
like this:
class Person + include XML::Mapping + + choice_node :if, 'name', :then, (text_node :name, 'name'), + :elsif, '@name', :then, (text_node :name, '@name'), + :else, (text_node :name, '.') +end + +### usage + +p1 = Person.load_from_xml(REXML::Document.new('<person name="Jim"/>').root) +=> #<Person:0x007ff64a1cd660 @name="Jim"> + +p2 = Person.load_from_xml(REXML::Document.new('<person><name>James</name></person>').root) +=> #<Person:0x007ff64a1c54b0 @name="James"> + +p3 = Person.load_from_xml(REXML::Document.new('<person>Suzy</person>').root) +=> #<Person:0x007ff64a1b6820 @name="Suzy"> + + +p1.save_to_xml.write($stdout) +<person><name>Jim</name></person> +p2.save_to_xml.write($stdout) +<person><name>James</name></person> +p3.save_to_xml.write($stdout) +<person><name>Suzy</name></person>+ +
Here all sub-nodes of the choice_nodes are single-attribute nodes
+(text_nodes) with the same attribute (name
). As you see, when
+writing persons to XML, the name is always stored in
+a <name> sub-element. Of course, this is because that alternative
+appears first in the choice_node.
Finally, all nodes support keyword arguments :reader and :writer +which allow you to extend or completely override the reading and/or writing +functionality of the node with your own code. The :reader as well as the +:writer argument must be a proc that takes as its arguments the Ruby object +to be read/written (instance of the mapping class the node belongs to) and +the XML tree to be written to/read from. An optional +third argument may be specified – it will receive a proc that wraps the +default reader/writer functionality of the node.
+ +The :reader proc is for reading (from the XML into +the object), the :writer proc is for writing (from the object into the XML).
+ +Here's a (really contrived) example:
+ +class Foo + include XML::Mapping + + text_node :name, "@name", :reader=>proc{|obj,xml,default_reader| + default_reader.call(obj,xml) + obj.name += xml.attributes['more'] + }, + :writer=>proc{|obj,xml| + xml.attributes['bar'] = "hi #{obj.name} ho" + } +end + +f = Foo.load_from_xml(REXML::Document.new('<foo name="Jim" more="XYZ"/>').root) +=> #<Foo:0x007ff64a10e8c8 @name="JimXYZ"> + +xml = f.save_to_xml +xml.write $stdout,2 +<foo bar='hi JimXYZ ho'/>+ +
So there's a “Foo” class with a text_node that would by default
+(without the :reader and :writer proc) map the Ruby attribute “name” to the
+XML attribute “name”. The :reader proc is invoked
+when reading from XML into a Foo
+object. The xml
argument is the XML
+tree, obj
is the object. default_reader
is the
+proc that wraps the default reading functionality of the node. We invoke it
+at the beginning. For this text_node, the default reading functionality is
+to take the text of the “name” attribute of xml
and put it
+into the name
attribute of obj
. After that, we
+take the text of the “more” attribute of xml
and append it to
+the name
attribute of obj
. So the XML tree <foo name="Jim"
+more="XYZ"/>
is converted to a Foo
object
+with name
=“JimXYZ”.
In our :writer proc, we only take obj
(the Foo
+object to be written to XML) and xml
+(the XML tree the stuff is to be written to).
+Analogously to the :reader, we could take a proc that wraps the default
+writing functionality of the node, but we don't do that here–we
+completely override the writing functionality with our own code, which just
+takes the name
attribute of the object and writes “hi <the
+name> ho” to a bar
XML attribute in
+the XML tree (stupid example, I know).
As a special convention, if you specify both a :reader and a :writer for a
+node, and in both cases you do /not/ call the default behaviour, then you
+should use the generic node type node
, e.g.:
class SomeClass + include XML::Mapping + + .... + + node :reader=>proc{|obj,xml| ...}, + :writer=>proc{|obj,xml| ...} +end+ +
(since you're completely replacing both the reading and the writing +functionality, you're effectively replacing all the functionality of +the node, so it would be pointless and confusing to use one of the more +“specific” node types)
+ +As you see, the purpose of readers and writers is to make it possible to
+augment or override a node's functionality arbitrarily, so there
+shouldn't be anything that's absolutely impossible to achieve with
+xml-mapping. However, if you use readers and writers without invoking the
+default behaviour, you really do everything manually, so you're not
+doing any less work than you would do if you weren't using xml-mapping
+at all. So you'll probably use readers and/or writers for those bits of
+your mapping semantics that can't be achieved with xml-mapping's
+predefined node types (an alternative approach might be to override the
+post_load
and/or post_save
instance methods on
+the mapping class – see the reference documentation).
An advice similar to the one given above for marshallers/unmarshallers +applies here as well: If you find yourself writing lots of readers and +writers that only differ in some easily parameterizable aspects, you should +think about defining your own node types. We talk about that below, and it generally just means that you +move the (sensibly parameterized) code from your readers/writers to your +node types.
+ +Sometimes you might want to represent the same Ruby object in multiple +alternative ways in XML. For example, the name of a +“Person” object could be represented either in a “name” element or a “name” +attribute.
+ +xml-mapping supports this by allowing you to define multiple disjoint
+“mappings” for a mapping class. A mapping is by convention identified with
+a symbol, e.g. :my_mapping
, :other_mapping
etc.,
+and each mapping comprises a root element name and a set of node
+definitions. In the body of a mapping class definition, you switch to
+another mapping with use_mapping :the_mapping
. All following
+node declarations will be added to that mapping unless you specify
+the option :mapping=>:another_mapping for a node declaration (all node
+types support that option). The default mapping (the mapping used if there
+was no previous use_mapping
in the class body) is named
+:_default
.
All the worker methods like load_from_xml/file
,
+save_to_xml/file
, load_object_from_xml/file
+support a :mapping
keyword argument to specify the mapping,
+which again defaults to :_default
.
In the following example, we define two mappings (the default one and a
+mapping named :other
) for Person
objects with a
+name, an age and an address:
require 'xml/mapping' + +class Address; end + +class Person + include XML::Mapping + + # the default mapping. Stores the name and age in XML attributes, + # and the address in a sub-element "address". + + text_node :name, "@name" + numeric_node :age, "@age" + object_node :address, "address", :class=>Address + + use_mapping :other + + # the ":other" mapping. Non-default root element name; name and age + # stored in XML elements; address stored in the person's element + # itself + + root_element_name "individual" + text_node :name, "name" + numeric_node :age, "age" + object_node :address, ".", :class=>Address + + # you could also specify the mapping on a per-node basis with the + # :mapping option, e.g.: + # + # numeric_node :age, "age", :mapping=>:other +end + + +class Address + include XML::Mapping + + # the default mapping. + + text_node :street, "street" + numeric_node :number, "number" + text_node :city, "city" + numeric_node :zip, "zip" + + use_mapping :other + + # the ":other" mapping. + + text_node :street, "street-name" + numeric_node :number, "street-name/@number" + text_node :city, "city-name" + numeric_node :zip, "city-name/@zip-code" +end + + +### usage + +## XML representation of a person in the default mapping +xml = REXML::Document.new(' +<person name="Suzy" age="28"> + <address> + <street>Abbey Road</street> + <number>72</number> + <city>London</city> + <zip>18827</zip> + </address> +</person>').root + +## load using the default mapping +p = Person.load_from_xml xml +=> #<Person:0x007ff64a23e9c8 @name="Suzy", @age=28, @address=#<Address:0x007ff64a23d4b0 @street="Abbey Road", @number=72, @city="London", @zip=18827>> + +## save using the default mapping +xml2 = p.save_to_xml +xml2.write $stdout,2 +<person name='Suzy' age='28'> + <address> + <street> + Abbey Road + </street> + <number> + 72 + </number> + <city> + London + </city> + <zip> + 18827 + </zip> + </address> +</person> +## xml2 identical to xml + + +## now, save the same person to XML using the :other mapping... +other_xml = p.save_to_xml :mapping=>:other +other_xml.write $stdout,2 +<individual> + <name> + Suzy + </name> + <age> + 28 + </age> + <street-name number='72'> + Abbey Road + </street-name> + <city-name zip-code='18827'> + London + </city-name> +</individual> +## load it again using the :other mapping +p2 = Person.load_from_xml other_xml, :mapping=>:other +=> #<Person:0x007ff64a20c838 @name="Suzy", @age=28, @address=#<Address:0x007ff64a2079a0 @street="Abbey Road", @number=72, @city="London", @zip=18827>> + +## p2 identical to p+ +
In this example, each of the two mappings contains nodes that map the same +set of Ruby attributes (name, age and address). This is probably what you +want most of the time (since you're normally defining multiple XML mappings for the same Ruby data), but it's not +a necessity at all. When a mapping class is defined, xml-mapping will add +all Ruby attributes from all mappings to it.
+ +You may have noticed that the object_node
s in the
+Person
class apply the mapping they were themselves defined in
+to their sub-ordinated class (Address
). This is the case for
+all Single-attribute Nodes with Sub-objects
+(object_node
, array_node
and
+hash_node
) unless you explicitly specify a different mapping
+for the sub-object(s) using the option :sub_mapping, e.g.
object_node :address, "address", :class=>Address, :sub_mapping=>:other ++ +
It's easy to write additional node types and register them with the
+xml-mapping library (the following node types come with xml-mapping:
+node
, text_node
, numeric_node
,
+boolean_node
, object_node
,
+array_node
, hash_node
, choice_node
).
I'll first show an example, then some more theoretical insight.
+ +Let's say we want to extend the Signature
class from the
+example to include the time at which the signature was created. We want the
+new XML representation of such a signature to look
+like this:
<Signature> + <Name>John Doe</Name> + <Position>product manager</Position> + <signed-on> + <day>13</day> + <month>2</month> + <year>2005</year> + </signed-on> +</Signature>+ +
(we only save year, month and day to make this example shorter), and the +mapping class declaration to look like this:
+ +class Signature + include XML::Mapping + + text_node :name, "Name" + text_node :position, "Position", :default_value=>"Some Employee" + time_node :signed_on, "signed-on", :default_value=>Time.now +end ++ +
(i.e. a new “time_node” declaration was added).
+ +We want this time_node
call to define an attribute named
+signed_on
which holds the date value from the XML document in an instance of class Time
.
This node type can be defined with this piece of code:
+ +require 'xml/mapping/base' + +class TimeNode < XML::Mapping::SingleAttributeNode + def initialize(*args) + path,*args = super(*args) + @y_path = XML::XXPath.new(path+"/year") + @m_path = XML::XXPath.new(path+"/month") + @d_path = XML::XXPath.new(path+"/day") + args + end + + def extract_attr_value(xml) + y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i, + @m_path.first(xml).text.to_i, + @d_path.first(xml).text.to_i] + } + Time.local(y,m,d) + end + + def set_attr_value(xml, value) + @y_path.first(xml,:ensure_created=>true).text = value.year + @m_path.first(xml,:ensure_created=>true).text = value.month + @d_path.first(xml,:ensure_created=>true).text = value.day + end +end + + +XML::Mapping.add_node_class TimeNode ++ +
The last line registers the new node type with the xml-mapping library. The +name of the node factory method (“time_node”) is automatically derived from +the class name of the node type (“TimeNode”).
+ +There will be one instance of the node type TimeNode
per
+time_node
declaration per mapping class (not per mapping class
+instance). That instance (the “node” for short) will be created by the node
+factory method (time_node
); there's no need to instantiate
+the node type directly. The time_node
method places the node
+into the mapping class; the @owner attribute of the node is set to
+reference the mapping class. The node factory method passes the mapping
+class the node appears in (Signature
), followed by its own
+arguments, to the node's constructor. In the example, the
+time_node
method calls TimeNode.new(Signature,
+:signed_on, "signed-on", :default_value=>Time.now)
).
+new
of course creates the node and then delegates the
+arguments to our initializer initialize
. We first call the
+superclass's initializer, which strips off from the argument list those
+arguments it handles itself, and returns the remaining ones. In this case,
+the superclass XML::Mapping::SingleAttributeNode
+handles the Signature
, :signed_on
and
+:default_value=>Time.now
arguments – Signature
+is stored into @owner
, :signed_on
is stored into
+@attrname
, and {:default_value=>Time.now}
is
+stored into @options
. The remaining argument list
+["signed-on"]
is returned; we capture the
+"signed-on"
string in path (the rest of the
+argument list (an empty array) we capture in args for returning it
+at the end of the initializer. This isn't strictly necessary, it's
+just a convention that a node class initializer should always return those
+arguments it didn't handle itself). We'll interpret path
+as an XPath expression that locates the time value relative to the parent
+mapping object's XML tree (in this case, this
+would be the XML tree rooted at the
+<Signature>
element, i.e. the tree the
+Signature
instance was read from). We'll later have to
+read/store the year, month, and day values from
+path+"/year"
, path+"/month"
,
+and path+"/day"
, respectively, so we create (and
+precompile) three corresponding XPath expressions using XML::XXPath.new and store them into
+member variables of the node. XML::XXPath is
+an XPath implementation that is bundled with xml-mapping. It is very
+incomplete, but it supports writing (not just reading) of XML nodes, which is needed to support writing data back
+to XML. The XML::XXPath library is explained in more detail
+below.
The extract_attr_value
method is called whenever an instance
+of the mapping class the node belongs to (Signature
in the
+example) is being created from an XML tree. The
+parameter xml is that tree (again, this is the tree rooted at the
+<Signature>
element in this example). The method
+implementation is expected to extract the single attribute's value from
+xml and return it, or raise XML::Mapping::SingleAttributeNode::NoAttrValueSet
+if the attribute was “unset” in the XML (this
+exception tells the framework that the default value should be put in place
+if it was defined), or raise any other exception to signal an error and
+abort the whole process. Our superclass XML::Mapping::SingleAttributeNode
+will store the returned single attribute's value into the
+signed_on
attribute of the Signature
instance
+being read in. In our implementation, we apply the xpath expressions
+created during initialization to xml (e.g.
+@y_path.first(xml)
). An expression
+xpath_expr.first(xml) returns (as a REXML element) the first sub-element of xml
+that matches xpath_expr, or raises XML::XXPathError if there was no such
+element. We apply REXML's text method to the returned element
+to get out the element's text, convert it to integer, and supply it to
+the constructor of the Time
object to be returned. As a side
+note, if an XPath expression matches XML attributes,
+XML::XXPath methods like first will
+return XML::XXPath::Accessors::Attribute
+nodes that behave similarly to REXML::Element nodes, including support for
+messages like name and text, so this would've worked
+also if our XPath expressions had referred to XML
+attributes, not elements. The default_when_xpath_err
thing
+calls the supplied block and returns its value, but maps the exception XML::XXPathError to the mentioned XML::Mapping::SingleAttributeNode::NoAttrValueSet
+(any other exceptions fall through unchanged). As said above, XML::Mapping::SingleAttributeNode::NoAttrValueSet
+is caught by the framework (more precisely, by our superclass XML::Mapping::SingleAttributeNode),
+and the default value is set if it was provided. So you should just wrap
+default_when_xpath_err
around any applications of XPath
+expressions whose non-presence in the XML you want
+to be considered a non-presence of the attribute you're trying to
+extract. (XML::XXPath is designed to know knothing about XML::Mapping, so it doesn't raise XML::Mapping::SingleAttributeNode::NoAttrValueSet
+directly)
The set_attr_value
method is called whenever an instance of
+the mapping class the node belongs to (Signature
in the
+example) is being stored into an XML tree. The
+xml parameter is the XML tree (a REXML element node; here this is again the tree
+rooted at the <Signature>
element); value is
+the current value of the single attribute (in this example, the
+signed_on
attribute of the Signature
instance
+being stored). xml will most probably be “half-populated” by the
+time this method is called – the framework calls the
+set_attr_value
methods of all nodes of a mapping class in the
+order of their definition, letting each node fill its “bit” into
+xml. The method implementation is expected to write value
+into (the correct sub-elements of) xml, or raise an exception to
+signal an error and abort the whole process. No default value handling is
+done here; set_attr_value
won't be called at all if the
+attribute had been set to its default value. In our implementation we grab
+the year, month and day values from value (which must be a
+Time
), and store it into the sub-elements of xml
+identified by XPath expressions @y_path
, @m_path
+and @d_path
, respectively. We do this by calling XML::XXPath#first with an
+additional parameter :ensure_created=>true
. An expression
+xpath_expr.first(xml,:ensure_created=>true) works just
+like xpath_expr.first(xml) if xpath_expr was
+already present in xml. If it was not, it is created (preferably
+at the end of xml's list of sub-nodes), and returned. See below for a more detailed documentation of the XPath
+interpreter.
As just said, XML::XXPath, when used to
+create new XML nodes, generally appends those nodes
+to the end of the list of subnodes of the node the xpath expression was
+applied to. All xml-mapping nodes that come with xml-mapping use XML::XXPath when writing data to XML, and therefore also append their data to the XML data written by preceding nodes (the nodes are
+invoked in the order of their definition). This means that, generally, your
+output data will appear in the XML document in the
+same order in which the corresponding xml-mapping node definitions appeared
+in the mapping class (unless you used XPath expressions like foo which explicitly dictate a fixed position in the
+sequence of XML nodes). For instance, in the
+Order
class from the example at the beginning of this
+document, if we put the :signatures
node before the
+:items
node, the <Signed-By>
element will
+appear before the sequence of <Item>
elements
+in the output XML.
The following is a more systematic overview of the basic node types. The +description is self-contained, so some information from the previous +section will be repeated.
+ +A node type is implemented as a Ruby class derived from XML::Mapping::Node or one of its +subclasses.
+ +The following node types (node classes) come with xml-mapping (they all +live in the XML::Mapping namespace, which +I've left out here for brevity):
+ +Node + +-SingleAttributeNode + | +-SubObjectBaseNode + | | +-ObjectNode + | | +-ArrayNode + | | +-HashNode + | +-TextNode + | +-NumericNode + | +-BooleanNode + +-ChoiceNode+ +
XML::Mapping::Node is the base class
+for all nodes, XML::Mapping::SingleAttributeNode
+is the base class for single-attribute nodes,
+and XML::Mapping::SubObjectBaseNode
+is the base class for single-attribute nodes
+with sub-objects. XML::Mapping::TextNode, XML::Mapping::ArrayNode etc. are of
+course the text_node
, array_node
etc. we've
+talked about in this document. When you've written a new node class,
+you register it with xml-mapping by calling
+XML::Mapping.add_node_class MyNode
. When you do that,
+xml-mapping automatically defines the node factory method for your class –
+the method's name (e.g. my_node
) is derived from the
+node's class name (e.g. Foo::Bar::MyNode) by stripping all parent
+module names, and then converting capital letters to lowercase and
+preceding them with an underscore. In fact, this is just how all the
+predefined node types are defined – those node types are not “special”;
+they're defined in the source file
+xml/mapping/standard_nodes.rb
and then registered normally in
+xml/mapping.rb
. The source code of the built-in nodes is not
+very long or complicated; you may consider reading it in addition to this
+text to gain a better understanding.
The xml-mapping core “operates” node types as follows:
+ +As said above, when a node class is registered with xml-mapping by calling
+XML::Mapping.add_node_class TheNodeClass
, xml-mapping
+automatically generates the node factory method for that type. The node
+factory method will effectively be defined as a class method of the XML::Mapping module, which is why one can call
+it from the body of a mapping class definition. The generated method will
+create a new instance of the node class (a node for short) by
+calling new on the node class. The list of parameters to
+new will consist of the mapping class, followed by all
+arguments that were passed to the node factory method. For example,
+when you have this node declaration:
class MyMappingClass + include XML::Mapping + + my_node :foo, "bar", 42, :hi=>"ho" +end ++ +
, then the node factory method (my_node
) calls
+MyNode.new(MyMappingClass, :foo, "bar", 42,
+:hi=>"ho")
.
new of course creates the instance and calls initialize +on it. The initialize implementation will generally store the +parameters into some instance variables for later usage. As a convention, +initialize should always extract from the parameter list those +parameters it processes itself, process them, and return an array +containing the remaining (still unprocessed) parameters. Thus, an +implementation of initialize follows this pattern:
+ +def initialize(*args) + myparam1,myparam2,...,myparamx,*args = super(*args) + + .... process the myparam1,myparam2,...,myparamx .... + + # return still unprocessed args + args +end+ +
(since the called superclass initializer is written the same way, the +parameter array returned by it will already be stripped of all parameters +that the superclass initializer (or any of its superclasses's +initializers) processed)
+ +This technique is a simple way to “chain” the initializers of all +superclasses of a node class, starting with the topmost one (Node), so that +each initializer can easily find out and process the parameters it is +responsible for.
+ +The base node class XML::Mapping::Node +provides an initialize implementation that, among other things +(described below), adds self (i.e. the created node) to the +internal list of nodes held by the mapping class, and sets the @owner +attribute of self to reference the mapping class.
+ +So, effectively there will be one instance of a node class (a node) per +node definition, and that instance lives in the mapping class the node was +defined in.
+ +When an instance of a mapping class is created or filled from an XML tree, xml-mapping will call xml_to_obj
+on all nodes defined in that mapping class in the mapping the node is defined in, in the order of
+their definition. Two parameters will be passed: the mapping class instance
+being created/filled, and the XML tree the instance
+is being created/filled from. The implementation of xml_to_obj
+is expected to read whatever pieces of data it is responsible for from the
+XML tree and put it into the appropriate
+variables/attributes etc. of the instance.
When an instance of a mapping class is stored or filled into an XML tree, xml-mapping will call obj_to_xml
+on all nodes defined in that mapping class in the mapping the node is defined in, in the order of
+their definition, again passing as parameters the mapping class instance
+being stored, and the XML tree the instance is being
+stored/filled into. The implementation of obj_to_xml
is
+expected to read whatever pieces of data it is responsible for from the
+instance and put it into the appropriate XML
+elements/XML attr etc. of the XML tree.
The following is an overview of how initialization and +marshalling/unmarshalling is implemented in the node base classes (Node, +SingleAttributeNode, and SubObjectBaseNode).
+ +TODO: summary table: member var name; introduced in class; meaning
+ +In initialize, the mapping class and the option arguments are
+stripped from the argument list. The mapping class is stored in @owner, the
+option arguments are stored (as a hash) in @options (the hash will be empty
+if no options were given). The mapping the node
+is defined in is determined (:mapping option, last use_mapping
+or :_default
) and stored in @mapping. The node then stores
+itself in the list of nodes of the mapping class belonging to the mapping
+(@owner.xml_mapping_nodes(:mapping=>@mapping)
; see XML::Mapping::ClassMethods#xml_mapping_nodes).
+This list is the list of nodes later used when marshalling/unmarshalling an
+instance of the mapping class with respect to a given mapping. This means
+that node implementors will not normally “see” anything of the mapping
+(they don't need to access the @mapping variable) because the
+marshalling/unmarshalling methods
+(obj_to_xml
/xml_to_obj
) simply won't be
+called if the node's mapping is not the same as the mapping the
+marshalling/unmarshalling is happening with.
Furthermore, if :reader and/or :writer options were given,
+xml_to_obj
resp. obj_to_xml
are transparently
+overwritten on the node to delegate to the supplied :reader/:writer procs.
The marshalling/unmarshalling methods
+(obj_to_xml
/xml_to_obj
) are not implemented in
+Node
(they just raise an exception).
In initialize, the attribute name is stripped from the argument +list and stored in @attrname, and an attribute of that name is added to the +mapping class the node belongs to.
+ +During marshalling/unmarshalling of an object to/from XML, single-attribute nodes only read/write a single
+piece of the object's state: the single attribute (@attrname) the node
+handles. Because of this, the
+obj_to_xml
/xml_to_obj
implementations in
+SingleAttributeNode call two new methods introduced by SingleAttributeNode,
+which must be overwritten by subclasses:
extract_attr_value(xml) + +set_attr_value(xml, value) ++ +
extract_attr_value(xml)
is called by xml_to_obj
+during unmarshalling. xml is the XML tree
+being read. The method must read the attribute's value from
+xml and return it. xml_to_obj
will set the attribute
+to that value.
set_attr_value(xml, value)
is called by
+obj_to_xml
during marshalling. xml is the XML tree being written, value is the current
+value of the attribute. The method must write value into (the
+correct sub-elements/attributes) of xml.
SingleAttributeNode also handles the default value, if it was specified
+(via the :default_value option): When writing data to XML, set_attr_value(xml, value)
won't
+be called if the attribute was set to the default value. When reading data
+from XML, the extract_attr_value(xml)
+implementation must raise a special exception, XML::Mapping::SingleAttributeNode::NoAttrValueSet,
+if it wants to indicate that the data was not present in the XML. SingleAttributeNode will catch this exception and
+put the default value, if it was defined, into the attribute.
The initializer will set up additional member variables @sub_mapping, +@marshaller, and @unmarshaller.
+ +@sub_mapping contains the mapping to be used when reading/writing the +sub-objects (either specified with :sub_mapping, or, by default, the +mapping the node itself was defined in).
+ +@marshaller and @unmarshaller contain procs that encapsulate
+writing/reading of sub-objects to/from XML, as
+specified by the user with :class/:marshaller/:unmarshaller etc. options
+(the meaning of those different options was described above). The procs are there to be called from
+extract_attr_value
or set_attr_value
whenever the
+need arises.
XML::XXPath is an XPath parser. It is used in
+xml-mapping node type definitions, but can just as well be utilized
+stand-alone (it does not depend on xml-mapping). XML::XXPath is very incomplete and probably will
+always be, but it should be reasonably efficient (XPath expressions are
+precompiled), and, most importantly, it supports write access, which is
+needed for writing objects to XML. For example, if
+you create the path /foo/bar[3]/baz[@key='hiho']
in the XML document
<foo> + <bar> + <baz key="ab">hello</baz> + <baz key="xy">goodbye</baz> + </bar> +</foo>+ +
, you'll get:
+ +<foo> + <bar> + <baz key='ab'>hello</baz> + <baz key='xy'>goodbye</baz> + </bar> + <bar/> + <bar> + <baz key='hiho'/> + </bar> +</foo>+ +
XML::XXPath is explained in more detail in +the reference documentation and the user_manual_xxpath file.
+ +xml-mapping is licensed under the Apache License, version 2.0. See the +LICENSE file for details.
+Xml-xxpath is an (incomplete) XPath interpreter that is at the moment
+bundled with xml-mapping. It is built on top of REXML. xml-mapping uses xml-xxpath extensively for
+implementing its node types – see the README
+file and the reference documentation (and the source code) for details.
+xml-xxpath, however, does not depend on xml-mapping at all, and is useful
+in its own right – maybe I'll later distribute it as a seperate library
+instead of bundling it. For the time being, if you want to use this XPath
+implementation stand-alone, you can just rip the files
+lib/xml/xxpath.rb
, lib/xml/xxpath/steps.rb
, and
+lib/xml/xxpath_methods.rb
out of the xml-mapping distribution
+and use them on their own (they do not depend on anything else).
xml-xxpath's XPath support is vastly incomplete (see below), but, in
+addition to the normal reading/matching functionality found in other XPath
+implementations (i.e. “find all elements in a given XML document matching a given XPath expression”),
+xml-xxpath supports write access. For example, when writing the
+XPath expression /foo/bar[3]/baz[@key='hiho']
to the XML document
<foo> + <bar> + <baz key='ab'>hello</baz> + <baz key='xy'>goodbye</baz> + </bar> +</foo>+ +
, you'll get:
+ +<foo> + <bar> + <baz key='ab'>hello</baz> + <baz key='xy'>goodbye</baz> + </bar> + <bar/> + <bar><baz key='hiho'/></bar> +</foo>+ +
This feature is used by xml-mapping when writing (marshalling) Ruby objects +to XML, and is actually the reason why I +couldn't just use any of the existing XPath implementations, e.g. the +one that comes with REXML. Also, the whole +xml-xxpath implementation is just 300 lines of Ruby code, it is quite fast +(paths are precompiled), and xml-xxpath returns matched elements in the +order they appeared in the source document – I've heard REXML::XPath +doesn't do that :)
+ +Some basic knowledge of XPath is helpful for reading this document.
+ +At the moment, xml-xxpath understands XPath expressions of the form
+[/
]pathelement/pathelement/…, where each pathelement must be
+one of these:
a simple element name name, e.g. signature
an attribute name, @attrname, e.g. @key
a combination of an element name and an attribute name and -value, in the
+form elt_name[@attr_name='attr_value']
an element name and an index, elt_name[index]
the "match-all" path element, *
.
+name1|
name2|
...
.[@key='xy'] / self::*[@key='xy']
child::*[@key='xy']
text()
Xml-xxpath only supports relative paths at this time, i.e. XPath +expressions beginning with “/” or “//” will still only find nodes below the +node the expression is applied to (as if you had written “./” or “.//”, +respectively).
+ +Xml-xxpath defines the class XML::XXPath. An +instance of that class wraps an XPath expression, the string representation +of which must be supplied when constructing the instance. You then call +instance methods like first, all or create_new +on the instance, supplying the REXML Element the +XPath expression should be applied to, and get the results, or, in the case +of write access, the element is updated in-place.
+ +require 'xml/xxpath' + +d=REXML::Document.new <<EOS + <foo> + <bar> + <baz key="work">Java</baz> + <baz key="play">Ruby</baz> + </bar> + <bar> + <baz key="ab">hello</baz> + <baz key="play">scrabble</baz> + <baz key="xy">goodbye</baz> + </bar> + <more> + <baz key="play">poker</baz> + </more> + </foo> +EOS + +####read access +path=XML::XXPath.new("/foo/bar[2]/baz") + +## path.all(document) gives all elements matching path in document +path.all(d) +=> [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>] + +## loop over them +path.each(d){|elt| puts elt.text} +hello +scrabble +goodbye +=> [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>] + +## the first of those +path.first(d) +=> <baz key='ab'> ... </> + +## no match here (only three "baz" elements) +path2=XML::XXPath.new("/foo/bar[2]/baz[4]") +path2.all(d) +=> [] + +## "first" raises XML::XXPathError in such cases... +path2.first(d) +XML::XXPathError: path not found: /foo/bar[2]/baz[4] + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:75:in `first' + +##...unless we allow nil returns +path2.first(d,:allow_nil=>true) +=> nil + +##attribute nodes can also be returned +keysPath=XML::XXPath.new("/foo / @key") + +keysPath.all(d).map{|attr|attr.text} +=> ["work", "play", "ab", "play", "xy", "play"]+ +
The objects supplied to the all()
, first()
, and
+each()
calls must be REXML element
+nodes, i.e. they must support messages like elements
,
+attributes
etc (instances of REXML::Element and its subclasses do this).
+The calls return the found elements as instances of REXML::Element or XML::XXPath::Accessors::Attribute.
+The latter is a wrapper around attribute nodes that is largely
+call-compatible to REXML::Element. This is
+so you can write things like path.each{|node|puts node.text}
+without having to special-case anything even if the path matches
+attributes, not just elements.
As you can see, you can re-use path objects, applying them to different XML elements at will. You should do this because the +XPath pattern is stored inside the XPath object in a pre-compiled form, +which makes it more efficient.
+ +The path elements of the XPath pattern are applied to the
+.elements
collection of the passed XML
+element and its sub-elements, starting with the first one. This is shown by
+the following code:
require 'xml/xxpath' + +d=REXML::Document.new <<EOS + <foo> + <bar x="hello"> + <first> + <second>pingpong</second> + </first> + </bar> + <bar x="goodbye"/> + </foo> +EOS + +XML::XXPath.new("/foo/bar").all(d) +=> [<bar x='hello'> ... </>, <bar x='goodbye'/>] + +XML::XXPath.new("/bar").all(d) +=> [] + +XML::XXPath.new("/foo/bar").all(d.root) +=> [] + +XML::XXPath.new("/bar").all(d.root) +=> [<bar x='hello'> ... </>, <bar x='goodbye'/>] + +firstelt = XML::XXPath.new("/foo/bar/first").first(d) +=> <first> ... </> + +XML::XXPath.new("/first/second").all(firstelt) +=> [] + +XML::XXPath.new("/second").all(firstelt) +=> [<second> ... </>]+ +
A REXML Document
object is a REXML Element
object whose
+elements
collection consists only of a single member – the
+document's root node. The first path element of the XPath – “foo” in
+the example – is matched against that. That is why the path “/bar” in the
+example doesn't match anything when matched against the document
+d
itself.
An ordinary REXML Element
object that
+represents a node somewhere inside an XML tree has
+an elements
collection that consists of all the element's
+direct sub-elements. That is why XPath patterns matched against the
+firstelt
element in the example must not start with
+“/first” (unless there is a child node that is also named “first”).
You may pass an :ensure_created=>true
option argument to
+path.first(elt) / path.all(elt) calls
+to make sure that path exists inside the passed XML element elt. If it existed before, nothing
+changes, and the call behaves just as it would without the option argument.
+If the path didn't exist before, the XML element
+is modified such that
the path exists afterwards
+all paths that existed before still exist afterwards
+the modification is as small as possible (i.e. as few elements as possible +are added, additional attributes are added to existing elements if +possible etc.)
+The created resp. previously existing, matching elements are returned.
+ +Examples:
+ +require 'xml/xxpath' + +d=REXML::Document.new <<EOS + <foo> + <bar> + <baz key="work">Java</baz> + <baz key="play">Ruby</baz> + </bar> + </foo> +EOS + +rootelt=d.root + +#### ensuring that a specific path exists inside the document + +XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true) +=> <baz key='work'> ... </> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> +</foo>### no change (path existed before) + +XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true) +=> <baz key='42'/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + </bar> +</foo>### path was added + +XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true) +=> <baz key='42'/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + </bar> +</foo>### no change this time + +XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true) +=> <baz key='work' key2='hello'> ... </> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='hello'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + </bar> +</foo>### this fit in the 1st "baz" element since +### there was no "key2" attribute there before. + +XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true) +=> <baz key='play'> ... </> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='hello'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + </bar> +</foo>### no change + +XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true) +=> #<XML::XXPath::Accessors::Attribute:0x007ff64a13ed48 @parent=<baz haha='[unset]'/>, @name="haha"> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='hello'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + <baz/> + <baz/> + <baz haha='[unset]'/> + </bar> +</foo>### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements + +XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true) +=> #<XML::XXPath::Accessors::Attribute:0x007ff64a12e240 @parent=<baz haha='[unset]'/>, @name="haha"> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='hello'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + <baz key='42'/> + <baz/> + <baz/> + <baz haha='[unset]'/> + </bar> +</foo>### no change this time+ +
Alternatively, you may pass a :create_new=>true
option
+argument or call create_new
+(path.create_new(
elt)
is
+equivalent to
+path.first(
elt,:create_new=>true)
).
+In that case, a new node is created in elt for each path element
+of path (or an exception raised if that wasn't possible for
+any path element).
Examples:
+ +require 'xml/xxpath' + +d=REXML::Document.new <<EOS + <foo> + <bar> + <baz key="work">Java</baz> + <baz key="play">Ruby</baz> + </bar> + </foo> +EOS + +rootelt=d.root + +path1=XML::XXPath.new("/bar/baz[@key='work']") + +path1.create_new(rootelt) +=> <baz key='work'/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> +</foo>### a new element is created for *each* path element, regardless of +### what existed before. So a new "bar" element was added, with a new +### "baz" element inside it + +### same call again... +path1.create_new(rootelt) +=> <baz key='work'/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz key='work'/> + </bar> +</foo>### same procedure -- new elements added for each path element + +## get reference to 1st "baz" element +firstbazelt=XML::XXPath.new("/bar/baz").first(rootelt) +=> <baz key='work'> ... </> + +path2=XML::XXPath.new("@key2") + +path2.create_new(firstbazelt) +=> #<XML::XXPath::Accessors::Attribute:0x007ff649a6bc00 @parent=<baz key='work' key2='[unset]'> ... </>, @name="key2"> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='[unset]'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz key='work'/> + </bar> +</foo>### ok, new attribute node added + +### same call again... +path2.create_new(firstbazelt) +XML::XXPathError: XPath (@key2): create_new and attribute already exists + from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:215:in `create_on' + from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:80:in `block in creator' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `call' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `all' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:70:in `first' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:112:in `create_new' +### can't create that path anew again -- an element can't have more +### than one attribute with the same name + +### the document hasn't changed +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='[unset]'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz key='work'/> + </bar> +</foo> + +### create_new the same path as in the ensure_created example +baz6elt=XML::XXPath.new("/bar/baz[6]").create_new(rootelt) +=> <baz/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='[unset]'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + </bar> +</foo>### ok, new "bar" element and 6th "baz" element inside it created + +XML::XXPath.new("baz[6]").create_new(baz6elt.parent) +XML::XXPathError: XPath: baz[6]: create_new and element already exists + from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:167:in `create_on' + from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:80:in `block in creator' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `call' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `all' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:70:in `first' + from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:112:in `create_new' +### yep, baz[6] already existed and thus couldn't be created once +### again + +### but of course... +XML::XXPath.new("/bar/baz[6]").create_new(rootelt) +=> <baz/> +d.write($stdout,2) + +<foo> + <bar> + <baz key='work' key2='[unset]'> + Java + </baz> + <baz key='play'> + Ruby + </baz> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz key='work'/> + </bar> + <bar> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + </bar> + <bar> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + <baz/> + </bar> +</foo>### this works because *all* path elements are newly created+ +
This feature is used in xml-mapping by node types like XML::Mapping::ArrayNode, which must +create a new instance of the “per-array element path” for each element of +the array to be stored in an XML tree.
+ +What is created when the Path “*” is to be created inside an empty XML element? The name of the element to be created +isn't known, but still some element must be created. The answer is that +xml-xxpath creates a special “unspecified” element whose name must be set +by the caller afterwards:
+ +require 'xml/xxpath' + +d=REXML::Document.new <<EOS + <foo> + <bar/> + <bar/> + </foo> +EOS + +rootelt=d.root + +XML::XXPath.new("*").all(rootelt) +=> [<bar/>, <bar/>] +### ok + +XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true) +=> nil +### ok, nothing there + +### the same call with :ensure_created=>true +newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true) +=> </> + +d.write($stdout,2) + +<foo> + <bar> + </> + </bar> + <bar/> +</foo> +### a new "unspecified" element was created +newelt.unspecified? +=> true + +### we must modify it to "specify" it +newelt.name="new-one" +newelt.text="hello!" +newelt.unspecified? +=> false + +d.write($stdout,2) + +<foo> + <bar> + <new-one> + hello! + </new-one> + </bar> + <bar/> +</foo> +### you could also set unspecified to false explicitly, as in: +newelt.unspecified=true+ +
The “newelt” object in the last example is an ordinary REXML::Element. xml-xxpath mixes the +“unspecified” attribute into that class, as well as into the XML::XXPath::Accessors::Attribute +class mentioned above.
+ +doc/xpath_impl_notes.txt
contains some documentation on the
+implementation of xml-xxpath.
Ruby's.
+