diff --git a/lib/rubyXL/objects/ooxml_object.rb b/lib/rubyXL/objects/ooxml_object.rb index 9a9d3e9ee..2d47cb257 100644 --- a/lib/rubyXL/objects/ooxml_object.rb +++ b/lib/rubyXL/objects/ooxml_object.rb @@ -18,6 +18,59 @@ def obtain_class_variable(var_name, default = {}) self.class_variable_set(var_name, default) end + # This is like attr_accessor method, creating the getter and setter + # However, in this case the setter update the hash of the object containing it. + # It also trigger the parent in case of change + def hash_attr_accessor symbol + class_eval <<-RUBY, __FILE__, __LINE__ + def #{symbol} + @#{symbol} + end + + def #{symbol}= value + str_hash = "#{symbol}".hash + + before_hash = @hash + @hash ^= @#{symbol}.hash * str_hash # Outmerge the hash component + @#{symbol} = value + @hash ^= value.hash * str_hash # Merge again the new hash component + + @parent.hash_dirty! if @parent + + value + end + RUBY + end + + # This is like attr_accessor method, creating the getter and setter + # However, in this case the setter update the hash of the object containing it. + # It also register the parent link on setup + def hash_attr_accessor_child_node symbol + class_eval <<-RUBY, __FILE__, __LINE__ + def #{symbol} + @#{symbol} + end + + def #{symbol}= new_value + str_hash = "#{symbol}".hash + + old_value = @#{symbol} + + old_value.parent = nil if old_value.is_a?(OOXMLObjectInstanceMethods) + new_value.parent = self if new_value.is_a?(OOXMLObjectInstanceMethods) + + before_hash = @hash + @hash ^= old_value.hash * str_hash # Outmerge the hash component + @#{symbol} = new_value + @hash ^= new_value.hash * str_hash # Merge again the new hash component + + @parent.hash_dirty! if @parent + + new_value + end + RUBY + end + # Defines an attribute of OOXML object. # === Parameters # * +attribute_name+ - Name of the element attribute as seen in the source XML. Can be either "String" or :Symbol @@ -50,7 +103,7 @@ def define_attribute(attr_name, attr_type, extra_params = {}) attr_hash = extra_params.merge({ :attr_type => attr_type }) attr_hash[:accessor] ||= accessorize(attr_name) attrs[attr_name.to_s] = attr_hash - self.send(:attr_accessor, attr_hash[:accessor]) unless attr_hash[:computed] + self.send(:hash_attr_accessor, attr_hash[:accessor]) unless attr_hash[:computed] end # Defines a child node of OOXML object. @@ -86,7 +139,7 @@ def define_child_node(klass, extra_params = {}) define_count_attribute if extra_params[:collection] == :with_count - self.send(:attr_accessor, accessor) + self.send(:hash_attr_accessor_child_node, accessor) end def define_count_attribute @@ -208,6 +261,10 @@ def process_attribute(obj, raw_value, params) module OOXMLObjectInstanceMethods attr_accessor :local_namespaces + # Accessor used to get back to the parent from this node, + # and dirtyize the parent in case of hash update. + attr_accessor :parent + def self.included(klass) klass.extend RubyXL::OOXMLObjectClassMethods end @@ -225,6 +282,7 @@ def initialize(params = {}) } init_child_nodes(params) + recompute_hash end def init_child_nodes(params) @@ -246,10 +304,27 @@ def preserve_whitespace end private :preserve_whitespace + # Manually update a hashing part. + def hash_dirty! + @need_hash_recompute = true + end + + # Recompute the hash with segment given by the childrens + def recompute_hash + @hash = 0 + @hash = obtain_class_variable(:@@ooxml_attributes).inject(@hash) { |h, (k, v)| h ^ ( v[:accessor].hash * self.send(v[:accessor]).hash ) } + @hash = obtain_class_variable(:@@ooxml_child_nodes).inject(@hash) { |h, (k, v)| h ^ ( v[:accessor].hash * self.send(v[:accessor]).hash ) } + @need_hash_recompute = false + end + + def hash + recompute_hash if @need_hash_recompute + @hash + end + def ==(other) other.is_a?(self.class) && - obtain_class_variable(:@@ooxml_attributes).all? { |k, v| self.send(v[:accessor]) == other.send(v[:accessor]) } && - obtain_class_variable(:@@ooxml_child_nodes).all? { |k, v| self.send(v[:accessor]) == other.send(v[:accessor]) } + self.hash == other.hash end # Recursively write the OOXML object and all its children out as Nokogiri::XML. Immediately before the actual