A PatternMatcher can be used to find, insert and remove patterns on a given model.
A pattern is specified by means of a block passed to the #add_pattern method. The block must take an Environment as first parameter and at least one model element as connection point as further parameter. The pattern matches if it can be found in a given environment and connected to the given connection point elements.
# File lib/rgen/util/pattern_matcher.rb, line 17 def initialize @patterns = {} @insert_mode = false @debug = false end
# File lib/rgen/util/pattern_matcher.rb, line 23 def add_pattern(name, &block) raise "a pattern needs at least 2 block parameters: " + "an RGen environment and a model element as connection point" \ unless block.arity >= 2 @patterns[name] = block end def find_pattern(env, name, *connection_points) match = find_pattern_internal(env, name, *connection_points) end def insert_pattern(env, name, *connection_points) @insert_mode = true root = evaluate_pattern(name, env, connection_points) @insert_mode = false root end def remove_pattern(env, name, *connection_points) match = find_pattern_internal(env, name, *connection_points) if match match.elements.each do |e| disconnect_element(e) env.delete(e) end match else nil end end def lazy(&block) if @insert_mode block.call else Lazy.new(&block) end end class Lazy < RGen::MetamodelBuilder::MMGeneric def initialize(&block) @block = block end def _eval @block.call end end private class Proxy < RGen::MetamodelBuilder::MMProxy attr_reader :_target def initialize(target) @_target = target end def method_missing(m, *args) result = @_target.send(m, *args) if result.is_a?(Array) result.collect do |e| if e.is_a?(RGen::MetamodelBuilder::MMBase) Proxy.new(e) else e end end else if result.is_a?(RGen::MetamodelBuilder::MMBase) Proxy.new(result) else result end end end end class Bindable < RGen::MetamodelBuilder::MMGeneric # by being an Enumerable, Bindables can be used for many-features as well include Enumerable def initialize @bound = false @value = nil @many = false end def _bound? @bound end def _many? @many end def _bind(value) @value = value @bound = true end def _value @value end def to_s @value.to_s end # pretend this is an enumerable which contains itself, so the bindable can be # inserted into many-features, when this is done the bindable is marked as a many-bindable def each @many = true yield(self) end def method_missing(m, *args) raise "bindable not bound" unless _bound? @value.send(m, *args) end end TempEnv = RGen::Environment.new class << TempEnv def <<(el); end end def find_pattern_internal(env, name, *connection_points) proxied_args = connection_points.collect{|a| a.is_a?(RGen::MetamodelBuilder::MMBase) ? Proxy.new(a) : a } bindables = create_bindables(name, connection_points) pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables) candidates = candidates_via_connection_points(pattern_root, connection_points) candidates ||= env.find(:class => pattern_root.class) candidates.each do |e| # create new bindables for every try, otherwise they can could be bound to old values bindables = create_bindables(name, connection_points) pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables) matched = match(pattern_root, e) return Match.new(e, matched, bindables.collect{|b| b._value}) if matched end nil end def create_bindables(pattern_name, connection_points) (1..(num_pattern_variables(pattern_name) - connection_points.size)).collect{|i| Bindable.new} end def candidates_via_connection_points(pattern_root, connection_points) @candidates_via_connection_points_refs ||= {} refs = (@candidates_via_connection_points_refs[pattern_root.class] ||= pattern_root.class.ecore.eAllReferences.reject{|r| r.derived || r.many || !r.eOpposite}) candidates = nil refs.each do |r| t = pattern_root.getGeneric(r.name) cp = t.is_a?(Proxy) && connection_points.find{|cp| cp.object_id == t._target.object_id} if cp elements = cp.getGenericAsArray(r.eOpposite.name) candidates = elements if candidates.nil? || elements.size < candidates.size end end candidates end def match(pat_element, test_element) visited = {} check_later = [] return false unless match_internal(pat_element, test_element, visited, check_later) while cl = check_later.shift pv, tv = cl.lazy._eval, cl.value if cl.feature.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(cl.feature, "wrong attribute value (lazy): #{pv} vs. #{tv}") return false end else if pv.is_a?(Proxy) unless pv._target.object_id == tv.object_id match_failed(f, "wrong target object") return false end else unless (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later)) return false end end end end visited.keys end CheckLater = Struct.new(:feature, :lazy, :value) def match_internal(pat_element, test_element, visited, check_later) return true if visited[test_element] visited[test_element] = true unless pat_element.class == test_element.class match_failed(nil, "wrong class: #{pat_element.class} vs #{test_element.class}") return false end all_structural_features(pat_element).each do |f| pat_values = pat_element.getGeneric(f.name) # nil values must be kept to support size check with Bindables pat_values = [ pat_values ] unless pat_values.is_a?(Array) test_values = test_element.getGeneric(f.name) test_values = [ test_values] unless test_values.is_a?(Array) if pat_values.size == 1 && pat_values.first.is_a?(Bindable) && pat_values.first._many? unless match_many_bindable(pat_values.first, test_values) return false end else unless pat_values.size == test_values.size match_failed(f, "wrong size #{pat_values.size} vs. #{test_values.size}") return false end pat_values.each_with_index do |pv,i| tv = test_values[i] if pv.is_a?(Lazy) check_later << CheckLater.new(f, pv, tv) elsif pv.is_a?(Bindable) if pv._bound? unless pv._value == tv match_failed(f, "value does not match bound value #{pv._value.class}:#{pv._value.object_id} vs. #{tv.class}:#{tv.object_id}") return false end else pv._bind(tv) end else if f.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(f, "wrong attribute value") return false end else if pv.is_a?(Proxy) unless pv._target.object_id == tv.object_id match_failed(f, "wrong target object") return false end else unless both_nil_or_match(pv, tv, visited, check_later) return false end end end end end end end true end def match_many_bindable(bindable, test_values) if bindable._bound? bindable._value.each_with_index do |pv,i| tv = test_values[i] if f.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(f, "wrong attribute value") return false end else unless both_nil_or_match(pv, tv, visited, check_later) return false end end end else bindable._bind(test_values.dup) end true end def both_nil_or_match(pv, tv, visited, check_later) (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later)) end def match_failed(f, msg) puts "match failed #{f&&f.eContainingClass.name}##{f&&f.name}: #{msg}" if @debug end def num_pattern_variables(name) prok = @patterns[name] prok.arity - 1 end def evaluate_pattern(name, env, connection_points) prok = @patterns[name] raise "unknown pattern #{name}" unless prok raise "wrong number of arguments, expected #{prok.arity-1} connection points)" \ unless connection_points.size == prok.arity-1 prok.call(env, *connection_points) end def disconnect_element(element) return if element.nil? all_structural_features(element).each do |f| if f.many element.setGeneric(f.name, []) else element.setGeneric(f.name, nil) end end end def all_structural_features(element) @all_structural_features ||= {} return @all_structural_features[element.class] if @all_structural_features[element.class] @all_structural_features[element.class] = element.class.ecore.eAllStructuralFeatures.reject{|f| f.derived} end end end
# File lib/rgen/util/pattern_matcher.rb, line 317 def all_structural_features(element) @all_structural_features ||= {} return @all_structural_features[element.class] if @all_structural_features[element.class] @all_structural_features[element.class] = element.class.ecore.eAllStructuralFeatures.reject{|f| f.derived} end
# File lib/rgen/util/pattern_matcher.rb, line 285 def both_nil_or_match(pv, tv, visited, check_later) (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later)) end
# File lib/rgen/util/pattern_matcher.rb, line 160 def candidates_via_connection_points(pattern_root, connection_points) @candidates_via_connection_points_refs ||= {} refs = (@candidates_via_connection_points_refs[pattern_root.class] ||= pattern_root.class.ecore.eAllReferences.reject{|r| r.derived || r.many || !r.eOpposite}) candidates = nil refs.each do |r| t = pattern_root.getGeneric(r.name) cp = t.is_a?(Proxy) && connection_points.find{|cp| cp.object_id == t._target.object_id} if cp elements = cp.getGenericAsArray(r.eOpposite.name) candidates = elements if candidates.nil? || elements.size < candidates.size end end candidates end
# File lib/rgen/util/pattern_matcher.rb, line 156 def create_bindables(pattern_name, connection_points) (1..(num_pattern_variables(pattern_name) - connection_points.size)).collect{|i| Bindable.new} end
# File lib/rgen/util/pattern_matcher.rb, line 306 def disconnect_element(element) return if element.nil? all_structural_features(element).each do |f| if f.many element.setGeneric(f.name, []) else element.setGeneric(f.name, nil) end end end
# File lib/rgen/util/pattern_matcher.rb, line 298 def evaluate_pattern(name, env, connection_points) prok = @patterns[name] raise "unknown pattern #{name}" unless prok raise "wrong number of arguments, expected #{prok.arity-1} connection points)" \ unless connection_points.size == prok.arity-1 prok.call(env, *connection_points) end def disconnect_element(element) return if element.nil? all_structural_features(element).each do |f| if f.many element.setGeneric(f.name, []) else element.setGeneric(f.name, nil) end end end def all_structural_features(element) @all_structural_features ||= {} return @all_structural_features[element.class] if @all_structural_features[element.class] @all_structural_features[element.class] = element.class.ecore.eAllStructuralFeatures.reject{|f| f.derived} end end
# File lib/rgen/util/pattern_matcher.rb, line 30 def find_pattern(env, name, *connection_points) match = find_pattern_internal(env, name, *connection_points) end
# File lib/rgen/util/pattern_matcher.rb, line 139 def find_pattern_internal(env, name, *connection_points) proxied_args = connection_points.collect{|a| a.is_a?(RGen::MetamodelBuilder::MMBase) ? Proxy.new(a) : a } bindables = create_bindables(name, connection_points) pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables) candidates = candidates_via_connection_points(pattern_root, connection_points) candidates ||= env.find(:class => pattern_root.class) candidates.each do |e| # create new bindables for every try, otherwise they can could be bound to old values bindables = create_bindables(name, connection_points) pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables) matched = match(pattern_root, e) return Match.new(e, matched, bindables.collect{|b| b._value}) if matched end nil end
# File lib/rgen/util/pattern_matcher.rb, line 34 def insert_pattern(env, name, *connection_points) @insert_mode = true root = evaluate_pattern(name, env, connection_points) @insert_mode = false root end
# File lib/rgen/util/pattern_matcher.rb, line 54 def lazy(&block) if @insert_mode block.call else Lazy.new(&block) end end
# File lib/rgen/util/pattern_matcher.rb, line 176 def match(pat_element, test_element) visited = {} check_later = [] return false unless match_internal(pat_element, test_element, visited, check_later) while cl = check_later.shift pv, tv = cl.lazy._eval, cl.value if cl.feature.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(cl.feature, "wrong attribute value (lazy): #{pv} vs. #{tv}") return false end else if pv.is_a?(Proxy) unless pv._target.object_id == tv.object_id match_failed(f, "wrong target object") return false end else unless (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later)) return false end end end end visited.keys end
# File lib/rgen/util/pattern_matcher.rb, line 289 def match_failed(f, msg) puts "match failed #{f&&f.eContainingClass.name}##{f&&f.name}: #{msg}" if @debug end
# File lib/rgen/util/pattern_matcher.rb, line 204 def match_internal(pat_element, test_element, visited, check_later) return true if visited[test_element] visited[test_element] = true unless pat_element.class == test_element.class match_failed(nil, "wrong class: #{pat_element.class} vs #{test_element.class}") return false end all_structural_features(pat_element).each do |f| pat_values = pat_element.getGeneric(f.name) # nil values must be kept to support size check with Bindables pat_values = [ pat_values ] unless pat_values.is_a?(Array) test_values = test_element.getGeneric(f.name) test_values = [ test_values] unless test_values.is_a?(Array) if pat_values.size == 1 && pat_values.first.is_a?(Bindable) && pat_values.first._many? unless match_many_bindable(pat_values.first, test_values) return false end else unless pat_values.size == test_values.size match_failed(f, "wrong size #{pat_values.size} vs. #{test_values.size}") return false end pat_values.each_with_index do |pv,i| tv = test_values[i] if pv.is_a?(Lazy) check_later << CheckLater.new(f, pv, tv) elsif pv.is_a?(Bindable) if pv._bound? unless pv._value == tv match_failed(f, "value does not match bound value #{pv._value.class}:#{pv._value.object_id} vs. #{tv.class}:#{tv.object_id}") return false end else pv._bind(tv) end else if f.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(f, "wrong attribute value") return false end else if pv.is_a?(Proxy) unless pv._target.object_id == tv.object_id match_failed(f, "wrong target object") return false end else unless both_nil_or_match(pv, tv, visited, check_later) return false end end end end end end end true end
# File lib/rgen/util/pattern_matcher.rb, line 264 def match_many_bindable(bindable, test_values) if bindable._bound? bindable._value.each_with_index do |pv,i| tv = test_values[i] if f.is_a?(RGen::ECore::EAttribute) unless pv == tv match_failed(f, "wrong attribute value") return false end else unless both_nil_or_match(pv, tv, visited, check_later) return false end end end else bindable._bind(test_values.dup) end true end
# File lib/rgen/util/pattern_matcher.rb, line 293 def num_pattern_variables(name) prok = @patterns[name] prok.arity - 1 end
# File lib/rgen/util/pattern_matcher.rb, line 41 def remove_pattern(env, name, *connection_points) match = find_pattern_internal(env, name, *connection_points) if match match.elements.each do |e| disconnect_element(e) env.delete(e) end match else nil end end