rgen 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +28 -0
- data/Rakefile +3 -4
- data/lib/ea_support/uml13_ea_metamodel.rb +3 -3
- data/lib/ea_support/uml13_ea_to_uml13.rb +33 -2
- data/lib/ea_support/uml13_to_uml13_ea.rb +7 -0
- data/lib/mmgen/mm_ext/ecore_mmgen_ext.rb +4 -4
- data/lib/mmgen/templates/metamodel_generator.tpl +143 -143
- data/lib/rgen/ecore/ecore.rb +11 -1
- data/lib/rgen/ecore/ecore_interface.rb +47 -0
- data/lib/rgen/ecore/ecore_to_ruby.rb +166 -0
- data/lib/rgen/ecore/{ecore_transformer.rb → ruby_to_ecore.rb} +11 -11
- data/lib/rgen/environment.rb +15 -2
- data/lib/rgen/fragment/dump_file_cache.rb +63 -0
- data/lib/rgen/fragment/fragmented_model.rb +139 -0
- data/lib/rgen/fragment/model_fragment.rb +268 -0
- data/lib/rgen/instantiator/abstract_xml_instantiator.rb +44 -72
- data/lib/rgen/instantiator/default_xml_instantiator.rb +2 -2
- data/lib/rgen/instantiator/ecore_xml_instantiator.rb +16 -1
- data/lib/rgen/instantiator/json_instantiator.rb +16 -2
- data/lib/rgen/instantiator/nodebased_xml_instantiator.rb +118 -138
- data/lib/rgen/instantiator/qualified_name_resolver.rb +5 -1
- data/lib/rgen/instantiator/reference_resolver.rb +126 -24
- data/lib/rgen/instantiator/xmi11_instantiator.rb +6 -2
- data/lib/rgen/metamodel_builder.rb +18 -6
- data/lib/rgen/metamodel_builder/builder_extensions.rb +431 -407
- data/lib/rgen/metamodel_builder/builder_runtime.rb +8 -8
- data/lib/rgen/metamodel_builder/constant_order_helper.rb +4 -4
- data/lib/rgen/metamodel_builder/data_types.rb +5 -1
- data/lib/rgen/metamodel_builder/intermediate/feature.rb +167 -0
- data/lib/rgen/metamodel_builder/module_extension.rb +2 -2
- data/lib/rgen/model_builder.rb +10 -5
- data/lib/rgen/model_builder/builder_context.rb +17 -1
- data/lib/rgen/serializer/opposite_reference_filter.rb +18 -0
- data/lib/rgen/serializer/qualified_name_provider.rb +45 -0
- data/lib/rgen/template_language/template_container.rb +3 -1
- data/lib/rgen/{auto_class_creator.rb → util/auto_class_creator.rb} +6 -1
- data/lib/rgen/util/cached_glob.rb +67 -0
- data/lib/rgen/util/file_cache_map.rb +104 -0
- data/lib/rgen/util/file_change_detector.rb +78 -0
- data/lib/rgen/{method_delegation.rb → util/method_delegation.rb} +18 -3
- data/lib/rgen/{model_comparator.rb → util/model_comparator.rb} +17 -5
- data/lib/rgen/{model_comparator_base.rb → util/model_comparator_base.rb} +6 -1
- data/lib/rgen/{model_dumper.rb → util/model_dumper.rb} +6 -1
- data/lib/rgen/{name_helper.rb → util/name_helper.rb} +6 -1
- data/lib/rgen/util/pattern_matcher.rb +329 -0
- data/lib/transformers/uml13_to_ecore.rb +103 -60
- data/test/ecore_self_test.rb +43 -42
- data/test/json_test.rb +15 -0
- data/test/metamodel_builder_test.rb +361 -206
- data/test/metamodel_from_ecore_test.rb +45 -0
- data/test/metamodel_order_test.rb +10 -4
- data/test/metamodel_roundtrip_test.rb +2 -2
- data/test/metamodel_roundtrip_test/TestModel_Regenerated.rb +1 -1
- data/test/metamodel_roundtrip_test/houseMetamodel_Regenerated.ecore +50 -50
- data/test/method_delegation_test.rb +9 -9
- data/test/model_builder/ecore_internal.rb +19 -9
- data/test/model_builder/serializer_test.rb +1 -1
- data/test/reference_resolver_test.rb +79 -12
- data/test/rgen_test.rb +2 -0
- data/test/template_language_test.rb +7 -0
- data/test/template_language_test/templates/callback_indent_test/a.tpl +12 -0
- data/test/template_language_test/templates/callback_indent_test/b.tpl +5 -0
- data/test/testmodel/ea_testmodel_regenerated.xml +588 -583
- data/test/transformer_test.rb +3 -3
- data/test/util/file_cache_map_test.rb +91 -0
- data/test/util/file_cache_map_test/testdir/fileA +1 -0
- data/test/util_test.rb +4 -0
- data/test/xml_instantiator_test.rb +139 -135
- metadata +49 -104
- data/lib/rgen/ecore/ecore_instantiator.rb +0 -31
- data/lib/rgen/metamodel_builder/metamodel_description.rb +0 -232
- data/redist/xmlscan/ChangeLog +0 -1301
- data/redist/xmlscan/README +0 -34
- data/redist/xmlscan/THANKS +0 -11
- data/redist/xmlscan/doc/changes.html +0 -74
- data/redist/xmlscan/doc/changes.rd +0 -80
- data/redist/xmlscan/doc/en/conformance.html +0 -136
- data/redist/xmlscan/doc/en/conformance.rd +0 -152
- data/redist/xmlscan/doc/en/manual.html +0 -356
- data/redist/xmlscan/doc/en/manual.rd +0 -402
- data/redist/xmlscan/doc/ja/conformance.ja.html +0 -118
- data/redist/xmlscan/doc/ja/conformance.ja.rd +0 -134
- data/redist/xmlscan/doc/ja/manual.ja.html +0 -325
- data/redist/xmlscan/doc/ja/manual.ja.rd +0 -370
- data/redist/xmlscan/doc/src/Makefile +0 -41
- data/redist/xmlscan/doc/src/conformance.rd.src +0 -256
- data/redist/xmlscan/doc/src/langsplit.rb +0 -110
- data/redist/xmlscan/doc/src/manual.rd.src +0 -614
- data/redist/xmlscan/install.rb +0 -41
- data/redist/xmlscan/lib/xmlscan/encoding.rb +0 -311
- data/redist/xmlscan/lib/xmlscan/htmlscan.rb +0 -289
- data/redist/xmlscan/lib/xmlscan/namespace.rb +0 -352
- data/redist/xmlscan/lib/xmlscan/parser.rb +0 -299
- data/redist/xmlscan/lib/xmlscan/scanner.rb +0 -1109
- data/redist/xmlscan/lib/xmlscan/version.rb +0 -22
- data/redist/xmlscan/lib/xmlscan/visitor.rb +0 -158
- data/redist/xmlscan/lib/xmlscan/xmlchar.rb +0 -441
- data/redist/xmlscan/memo/CONFORMANCE +0 -1249
- data/redist/xmlscan/memo/PRODUCTIONS +0 -195
- data/redist/xmlscan/memo/contentspec.ry +0 -335
- data/redist/xmlscan/samples/chibixml.rb +0 -105
- data/redist/xmlscan/samples/getxmlchar.rb +0 -122
- data/redist/xmlscan/samples/rexml.rb +0 -159
- data/redist/xmlscan/samples/xmlbench.rb +0 -88
- data/redist/xmlscan/samples/xmlbench/parser/chibixml.rb +0 -22
- data/redist/xmlscan/samples/xmlbench/parser/nqxml.rb +0 -29
- data/redist/xmlscan/samples/xmlbench/parser/rexml.rb +0 -62
- data/redist/xmlscan/samples/xmlbench/parser/xmlparser.rb +0 -22
- data/redist/xmlscan/samples/xmlbench/parser/xmlscan-0.0.10.rb +0 -62
- data/redist/xmlscan/samples/xmlbench/parser/xmlscan-chibixml.rb +0 -22
- data/redist/xmlscan/samples/xmlbench/parser/xmlscan-rexml.rb +0 -22
- data/redist/xmlscan/samples/xmlbench/parser/xmlscan.rb +0 -99
- data/redist/xmlscan/samples/xmlbench/xmlbench-lib.rb +0 -116
- data/redist/xmlscan/samples/xmlconftest.rb +0 -200
- data/redist/xmlscan/test.rb +0 -7
- data/redist/xmlscan/tests/deftestcase.rb +0 -73
- data/redist/xmlscan/tests/runtest.rb +0 -47
- data/redist/xmlscan/tests/testall.rb +0 -14
- data/redist/xmlscan/tests/testencoding.rb +0 -438
- data/redist/xmlscan/tests/testhtmlscan.rb +0 -752
- data/redist/xmlscan/tests/testnamespace.rb +0 -457
- data/redist/xmlscan/tests/testparser.rb +0 -591
- data/redist/xmlscan/tests/testscanner.rb +0 -1749
- data/redist/xmlscan/tests/testxmlchar.rb +0 -143
- data/redist/xmlscan/tests/visitor.rb +0 -34
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module RGen
|
|
2
|
+
|
|
3
|
+
module Util
|
|
4
|
+
|
|
5
|
+
# WARNING: the mechanism of taking timestamps of directories in order to find out if the
|
|
6
|
+
# content has changed doesn't work reliably across all kinds of filesystems
|
|
7
|
+
#
|
|
8
|
+
class CachedGlob
|
|
9
|
+
|
|
10
|
+
def initialize(dir_glob, file_glob)
|
|
11
|
+
@dir_glob = dir_glob
|
|
12
|
+
@file_glob = file_glob
|
|
13
|
+
@root_dirs = []
|
|
14
|
+
@dirs = {}
|
|
15
|
+
@files = {}
|
|
16
|
+
@timestamps = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# returns all files contained in directories matched by +dir_glob+ which match +file_glob+.
|
|
20
|
+
# +file_glob+ must be relative to +dir_glob+.
|
|
21
|
+
# dir_glob "*/a" with file_glob "**/*.txt" is basically equivalent with Dir.glob("*/a/**/*.txt")
|
|
22
|
+
# the idea is that the file glob will only be re-eavluated when the content of one of the
|
|
23
|
+
# directories matched by dir_glob has changed.
|
|
24
|
+
# this will only be faster than a normal Dir.glob if the number of dirs matched by dir_glob is
|
|
25
|
+
# relatively large and changes in files affect only a few of them at a time.
|
|
26
|
+
def glob
|
|
27
|
+
root_dirs = Dir.glob(@dir_glob)
|
|
28
|
+
(@root_dirs - root_dirs).each do |d|
|
|
29
|
+
remove_root_dir(d)
|
|
30
|
+
end
|
|
31
|
+
(@root_dirs & root_dirs).each do |d|
|
|
32
|
+
update_root_dir(d) if dir_changed?(d)
|
|
33
|
+
end
|
|
34
|
+
(root_dirs - @root_dirs).each do |d|
|
|
35
|
+
update_root_dir(d)
|
|
36
|
+
end
|
|
37
|
+
@root_dirs = root_dirs
|
|
38
|
+
@root_dirs.sort.collect{|d| @files[d]}.flatten
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def dir_changed?(dir)
|
|
44
|
+
@dirs[dir].any?{|d| File.mtime(d) != @timestamps[dir][d]}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_root_dir(dir)
|
|
48
|
+
@dirs[dir] = Dir.glob(dir+"/**/")
|
|
49
|
+
@files[dir] = Dir.glob(dir+"/"+@file_glob)
|
|
50
|
+
@timestamps[dir] = {}
|
|
51
|
+
@dirs[dir].each do |d|
|
|
52
|
+
@timestamps[dir][d] = File.mtime(d)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def remove_root_dir(dir)
|
|
57
|
+
@dirs.delete(dir)
|
|
58
|
+
@files.delete(dir)
|
|
59
|
+
@timestamps.delete(dir)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module RGen
|
|
5
|
+
|
|
6
|
+
module Util
|
|
7
|
+
|
|
8
|
+
# Implements a cache for storing and loading data associated with arbitrary files.
|
|
9
|
+
# The data is stored in cache files within a subfolder of the folder where
|
|
10
|
+
# the associated files exists.
|
|
11
|
+
# The cache files are protected with a checksum and loaded data will be
|
|
12
|
+
# invalid in case either the associated file are the cache file has changed.
|
|
13
|
+
#
|
|
14
|
+
class FileCacheMap
|
|
15
|
+
# optional program version info to be associated with the cache files
|
|
16
|
+
# if the program version changes, cached data will also be invalid
|
|
17
|
+
attr_accessor :version_info
|
|
18
|
+
|
|
19
|
+
# +cache_dir+ is the name of the subfolder where cache files are created
|
|
20
|
+
# +postfix+ is an extension appended to the original file name for
|
|
21
|
+
# creating the name of the cache file
|
|
22
|
+
def initialize(cache_dir, postfix)
|
|
23
|
+
@postfix = postfix
|
|
24
|
+
@cache_dir = cache_dir
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# load data associated with file +key_path+
|
|
28
|
+
# returns :invalid in case either the associated file or the cache file has changed
|
|
29
|
+
def load_data(key_path)
|
|
30
|
+
cf = cache_file(key_path)
|
|
31
|
+
return :invalid unless File.exist?(cf)
|
|
32
|
+
result = nil
|
|
33
|
+
File.open(cf, "rb") do |f|
|
|
34
|
+
header = f.read(41)
|
|
35
|
+
return :invalid unless header
|
|
36
|
+
checksum = header[0..39]
|
|
37
|
+
data = f.read
|
|
38
|
+
if calc_sha1(data) == checksum
|
|
39
|
+
if calc_sha1_keydata(key_path) == data[0..39]
|
|
40
|
+
result = data[41..-1]
|
|
41
|
+
else
|
|
42
|
+
result = :invalid
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
result = :invalid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# store data +value_data+ associated with file +key_path+
|
|
52
|
+
def store_data(key_path, value_data)
|
|
53
|
+
data = calc_sha1_keydata(key_path) + "\n" + value_data
|
|
54
|
+
data = calc_sha1(data) + "\n" + data
|
|
55
|
+
cf = cache_file(key_path)
|
|
56
|
+
FileUtils.mkdir(File.dirname(cf)) rescue Errno::EEXIST
|
|
57
|
+
File.open(cf, "wb") do |f|
|
|
58
|
+
f.write(data)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# remove cache files which are not associated with any file in +key_paths+
|
|
63
|
+
# will only remove files within +root_path+
|
|
64
|
+
def clean_unused(root_path, key_paths)
|
|
65
|
+
raise "key paths must be within root path" unless key_paths.all?{|p| p.index(root_path) == 0}
|
|
66
|
+
used_files = key_paths.collect{|p| cache_file(p)}
|
|
67
|
+
files = Dir[root_path+"/**/"+@cache_dir+"/*"+@postfix]
|
|
68
|
+
files.each do |f|
|
|
69
|
+
FileUtils.rm(f) unless used_files.include?(f)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def cache_file(path)
|
|
76
|
+
File.dirname(path) + "/"+@cache_dir+"/" + File.basename(path) + @postfix
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def calc_sha1(data)
|
|
80
|
+
sha1 = Digest::SHA1.new
|
|
81
|
+
sha1.update(data)
|
|
82
|
+
sha1.hexdigest
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def keyData(path)
|
|
86
|
+
File.read(path)+@version_info.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# this method is much faster than calling +keyData+ and putting the result in +calc_sha1+
|
|
90
|
+
# reason is probably that there are not so many big strings being created
|
|
91
|
+
def calc_sha1_keydata(path)
|
|
92
|
+
sha1 = Digest::SHA1.new
|
|
93
|
+
sha1.file(path)
|
|
94
|
+
sha1.update(@version_info.to_s)
|
|
95
|
+
sha1.hexdigest
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
3
|
+
module RGen
|
|
4
|
+
|
|
5
|
+
module Util
|
|
6
|
+
|
|
7
|
+
# The FileChangeDetector detects changes in a set of files.
|
|
8
|
+
# Changes are detected between successive calls to check_files with a give set of files.
|
|
9
|
+
# Changes include files being added, removed or having changed their content.
|
|
10
|
+
#
|
|
11
|
+
class FileChangeDetector
|
|
12
|
+
|
|
13
|
+
FileInfo = Struct.new(:timestamp, :digest)
|
|
14
|
+
|
|
15
|
+
# Create a FileChangeDetector, options include:
|
|
16
|
+
#
|
|
17
|
+
# :file_added
|
|
18
|
+
# a proc which is called when a file is added, receives the filename
|
|
19
|
+
#
|
|
20
|
+
# :file_removed
|
|
21
|
+
# a proc which is called when a file is removed, receives the filename
|
|
22
|
+
#
|
|
23
|
+
# :file_changed
|
|
24
|
+
# a proc which is called when a file is changed, receives the filename
|
|
25
|
+
#
|
|
26
|
+
def initialize(options={})
|
|
27
|
+
@file_added = options[:file_added]
|
|
28
|
+
@file_removed = options[:file_removed]
|
|
29
|
+
@file_changed = options[:file_changed]
|
|
30
|
+
@file_info = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Checks if any of the files has changed compared to the last call of check_files.
|
|
34
|
+
# When called for the first time on a new object, all files will be reported as being added.
|
|
35
|
+
#
|
|
36
|
+
def check_files(files)
|
|
37
|
+
files_before = @file_info.keys
|
|
38
|
+
used_files = {}
|
|
39
|
+
files.each do |file|
|
|
40
|
+
used_files[file] = true
|
|
41
|
+
if @file_info[file]
|
|
42
|
+
if @file_info[file].timestamp != File.mtime(file)
|
|
43
|
+
@file_info[file].timestamp = File.mtime(file)
|
|
44
|
+
digest = calc_digest(file)
|
|
45
|
+
if @file_info[file].digest != digest
|
|
46
|
+
@file_info[file].digest = digest
|
|
47
|
+
@file_changed && @file_changed.call(file)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
@file_info[file] = FileInfo.new
|
|
52
|
+
@file_info[file].timestamp = File.mtime(file)
|
|
53
|
+
@file_info[file].digest = calc_digest(file)
|
|
54
|
+
@file_added && @file_added.call(file)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
files_before.each do |file|
|
|
58
|
+
if !used_files[file]
|
|
59
|
+
@file_info.delete(file)
|
|
60
|
+
@file_removed && @file_removed.call(file)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def calc_digest(file)
|
|
68
|
+
sha1 = Digest::SHA1.new
|
|
69
|
+
sha1.file(file)
|
|
70
|
+
sha1.hexdigest
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end
|
|
78
|
+
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
module RGen
|
|
2
|
+
|
|
3
|
+
module Util
|
|
2
4
|
|
|
3
5
|
module MethodDelegation
|
|
4
6
|
|
|
@@ -46,7 +48,7 @@ class << self
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def createDelegatingMethod(object, method)
|
|
49
|
-
if object
|
|
51
|
+
if hasMethod(object, method)
|
|
50
52
|
object.instance_eval <<-END
|
|
51
53
|
class << self
|
|
52
54
|
alias #{aliasMethodName(method)} #{method}
|
|
@@ -71,7 +73,7 @@ class << self
|
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def removeDelegatingMethod(object, method)
|
|
74
|
-
if object
|
|
76
|
+
if hasMethod(object, aliasMethodName(method))
|
|
75
77
|
# there is an aliased original, restore it
|
|
76
78
|
object.instance_eval <<-END
|
|
77
79
|
class << self
|
|
@@ -89,6 +91,16 @@ class << self
|
|
|
89
91
|
end
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
def hasMethod(object, method)
|
|
95
|
+
# in Ruby 1.9, #methods returns symbols
|
|
96
|
+
if object.methods.first.is_a?(Symbol)
|
|
97
|
+
method = method.to_sym
|
|
98
|
+
else
|
|
99
|
+
method = method.to_s
|
|
100
|
+
end
|
|
101
|
+
object.methods.include?(method)
|
|
102
|
+
end
|
|
103
|
+
|
|
92
104
|
def aliasMethodName(method)
|
|
93
105
|
"#{method}_delegate_original"
|
|
94
106
|
end
|
|
@@ -96,4 +108,7 @@ end
|
|
|
96
108
|
|
|
97
109
|
end
|
|
98
110
|
|
|
99
|
-
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
|
|
@@ -2,6 +2,8 @@ require 'rgen/ecore/ecore'
|
|
|
2
2
|
|
|
3
3
|
module RGen
|
|
4
4
|
|
|
5
|
+
module Util
|
|
6
|
+
|
|
5
7
|
module ModelComparator
|
|
6
8
|
|
|
7
9
|
# This method compares to models regarding equality
|
|
@@ -27,10 +29,18 @@ def _modelEqual_internal(a, b, featureIgnoreList, path)
|
|
|
27
29
|
puts "#{path.inspect}\n Array size differs: #{a.size} vs. #{b.size}"
|
|
28
30
|
return false
|
|
29
31
|
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
begin
|
|
33
|
+
# in Ruby 1.9 every object has the <=> operator but the default one returns
|
|
34
|
+
# nil and thus sorting won't work (ArgumentError)
|
|
35
|
+
as = a.sort
|
|
36
|
+
rescue ArgumentError, NoMethodError
|
|
37
|
+
as = a
|
|
38
|
+
end
|
|
39
|
+
begin
|
|
40
|
+
bs = b.sort
|
|
41
|
+
rescue ArgumentError, NoMethodError
|
|
42
|
+
bs = b
|
|
43
|
+
end
|
|
34
44
|
a.each_index do |i|
|
|
35
45
|
return false unless _modelEqual_internal(as[i], bs[i], featureIgnoreList, path+[i])
|
|
36
46
|
end
|
|
@@ -53,4 +63,6 @@ end
|
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
end
|
|
56
|
-
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
module RGen
|
|
2
|
+
|
|
3
|
+
module Util
|
|
4
|
+
|
|
5
|
+
# A PatternMatcher can be used to find, insert and remove patterns on a given model.
|
|
6
|
+
#
|
|
7
|
+
# A pattern is specified by means of a block passed to the add_pattern method.
|
|
8
|
+
# The block must take an Environment as first parameter and at least one model element
|
|
9
|
+
# as connection point as further parameter. The pattern matches if it can be found
|
|
10
|
+
# in a given environment and connected to the given connection point elements.
|
|
11
|
+
#
|
|
12
|
+
class PatternMatcher
|
|
13
|
+
|
|
14
|
+
Match = Struct.new(:root, :elements, :bound_values)
|
|
15
|
+
attr_accessor :debug
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@patterns = {}
|
|
19
|
+
@insert_mode = false
|
|
20
|
+
@debug = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_pattern(name, &block)
|
|
24
|
+
raise "a pattern needs at least 2 block parameters: " +
|
|
25
|
+
"an RGen environment and a model element as connection point" \
|
|
26
|
+
unless block.arity >= 2
|
|
27
|
+
@patterns[name] = block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_pattern(env, name, *connection_points)
|
|
31
|
+
match = find_pattern_internal(env, name, *connection_points)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def insert_pattern(env, name, *connection_points)
|
|
35
|
+
@insert_mode = true
|
|
36
|
+
root = evaluate_pattern(name, env, connection_points)
|
|
37
|
+
@insert_mode = false
|
|
38
|
+
root
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remove_pattern(env, name, *connection_points)
|
|
42
|
+
match = find_pattern_internal(env, name, *connection_points)
|
|
43
|
+
if match
|
|
44
|
+
match.elements.each do |e|
|
|
45
|
+
disconnect_element(e)
|
|
46
|
+
env.delete(e)
|
|
47
|
+
end
|
|
48
|
+
match
|
|
49
|
+
else
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def lazy(&block)
|
|
55
|
+
if @insert_mode
|
|
56
|
+
block.call
|
|
57
|
+
else
|
|
58
|
+
Lazy.new(&block)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Lazy < RGen::MetamodelBuilder::MMGeneric
|
|
63
|
+
def initialize(&block)
|
|
64
|
+
@block = block
|
|
65
|
+
end
|
|
66
|
+
def _eval
|
|
67
|
+
@block.call
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
class Proxy < RGen::MetamodelBuilder::MMProxy
|
|
74
|
+
attr_reader :_target
|
|
75
|
+
def initialize(target)
|
|
76
|
+
@_target = target
|
|
77
|
+
end
|
|
78
|
+
def method_missing(m, *args)
|
|
79
|
+
result = @_target.send(m, *args)
|
|
80
|
+
if result.is_a?(Array)
|
|
81
|
+
result.collect do |e|
|
|
82
|
+
if e.is_a?(RGen::MetamodelBuilder::MMBase)
|
|
83
|
+
Proxy.new(e)
|
|
84
|
+
else
|
|
85
|
+
e
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
if result.is_a?(RGen::MetamodelBuilder::MMBase)
|
|
90
|
+
Proxy.new(result)
|
|
91
|
+
else
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class Bindable < RGen::MetamodelBuilder::MMGeneric
|
|
99
|
+
# by being an Enumerable, Bindables can be used for many-features as well
|
|
100
|
+
include Enumerable
|
|
101
|
+
def initialize
|
|
102
|
+
@bound = false
|
|
103
|
+
@value = nil
|
|
104
|
+
@many = false
|
|
105
|
+
end
|
|
106
|
+
def _bound?
|
|
107
|
+
@bound
|
|
108
|
+
end
|
|
109
|
+
def _many?
|
|
110
|
+
@many
|
|
111
|
+
end
|
|
112
|
+
def _bind(value)
|
|
113
|
+
@value = value
|
|
114
|
+
@bound = true
|
|
115
|
+
end
|
|
116
|
+
def _value
|
|
117
|
+
@value
|
|
118
|
+
end
|
|
119
|
+
def to_s
|
|
120
|
+
@value.to_s
|
|
121
|
+
end
|
|
122
|
+
# pretend this is an enumerable which contains itself, so the bindable can be
|
|
123
|
+
# inserted into many-features, when this is done the bindable is marked as a many-bindable
|
|
124
|
+
def each
|
|
125
|
+
@many = true
|
|
126
|
+
yield(self)
|
|
127
|
+
end
|
|
128
|
+
def method_missing(m, *args)
|
|
129
|
+
raise "bindable not bound" unless _bound?
|
|
130
|
+
@value.send(m, *args)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
TempEnv = RGen::Environment.new
|
|
135
|
+
class << TempEnv
|
|
136
|
+
def <<(el); end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def find_pattern_internal(env, name, *connection_points)
|
|
140
|
+
proxied_args = connection_points.collect{|a|
|
|
141
|
+
a.is_a?(RGen::MetamodelBuilder::MMBase) ? Proxy.new(a) : a }
|
|
142
|
+
bindables = create_bindables(name, connection_points)
|
|
143
|
+
pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables)
|
|
144
|
+
candidates = candidates_via_connection_points(pattern_root, connection_points)
|
|
145
|
+
candidates ||= env.find(:class => pattern_root.class)
|
|
146
|
+
candidates.each do |e|
|
|
147
|
+
# create new bindables for every try, otherwise they can could be bound to old values
|
|
148
|
+
bindables = create_bindables(name, connection_points)
|
|
149
|
+
pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables)
|
|
150
|
+
matched = match(pattern_root, e)
|
|
151
|
+
return Match.new(e, matched, bindables.collect{|b| b._value}) if matched
|
|
152
|
+
end
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def create_bindables(pattern_name, connection_points)
|
|
157
|
+
(1..(num_pattern_variables(pattern_name) - connection_points.size)).collect{|i| Bindable.new}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def candidates_via_connection_points(pattern_root, connection_points)
|
|
161
|
+
@candidates_via_connection_points_refs ||= {}
|
|
162
|
+
refs = (@candidates_via_connection_points_refs[pattern_root.class] ||=
|
|
163
|
+
pattern_root.class.ecore.eAllReferences.reject{|r| r.derived || r.many || !r.eOpposite})
|
|
164
|
+
candidates = nil
|
|
165
|
+
refs.each do |r|
|
|
166
|
+
t = pattern_root.getGeneric(r.name)
|
|
167
|
+
cp = t.is_a?(Proxy) && connection_points.find{|cp| cp.object_id == t._target.object_id}
|
|
168
|
+
if cp
|
|
169
|
+
elements = cp.getGenericAsArray(r.eOpposite.name)
|
|
170
|
+
candidates = elements if candidates.nil? || elements.size < candidates.size
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
candidates
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def match(pat_element, test_element)
|
|
177
|
+
visited = {}
|
|
178
|
+
check_later = []
|
|
179
|
+
return false unless match_internal(pat_element, test_element, visited, check_later)
|
|
180
|
+
while cl = check_later.shift
|
|
181
|
+
pv, tv = cl.lazy._eval, cl.value
|
|
182
|
+
if cl.feature.is_a?(RGen::ECore::EAttribute)
|
|
183
|
+
unless pv == tv
|
|
184
|
+
match_failed(cl.feature, "wrong attribute value (lazy): #{pv} vs. #{tv}")
|
|
185
|
+
return false
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
if pv.is_a?(Proxy)
|
|
189
|
+
unless pv._target.object_id == tv.object_id
|
|
190
|
+
match_failed(f, "wrong target object")
|
|
191
|
+
return false
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
unless (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later))
|
|
195
|
+
return false
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
visited.keys
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
CheckLater = Struct.new(:feature, :lazy, :value)
|
|
204
|
+
def match_internal(pat_element, test_element, visited, check_later)
|
|
205
|
+
return true if visited[test_element]
|
|
206
|
+
visited[test_element] = true
|
|
207
|
+
unless pat_element.class == test_element.class
|
|
208
|
+
match_failed(nil, "wrong class: #{pat_element.class} vs #{test_element.class}")
|
|
209
|
+
return false
|
|
210
|
+
end
|
|
211
|
+
all_structural_features(pat_element).each do |f|
|
|
212
|
+
pat_values = pat_element.getGeneric(f.name)
|
|
213
|
+
# nil values must be kept to support size check with Bindables
|
|
214
|
+
pat_values = [ pat_values ] unless pat_values.is_a?(Array)
|
|
215
|
+
test_values = test_element.getGeneric(f.name)
|
|
216
|
+
test_values = [ test_values] unless test_values.is_a?(Array)
|
|
217
|
+
if pat_values.size == 1 && pat_values.first.is_a?(Bindable) && pat_values.first._many?
|
|
218
|
+
unless match_many_bindable(pat_values.first, test_values)
|
|
219
|
+
return false
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
unless pat_values.size == test_values.size
|
|
223
|
+
match_failed(f, "wrong size #{pat_values.size} vs. #{test_values.size}")
|
|
224
|
+
return false
|
|
225
|
+
end
|
|
226
|
+
pat_values.each_with_index do |pv,i|
|
|
227
|
+
tv = test_values[i]
|
|
228
|
+
if pv.is_a?(Lazy)
|
|
229
|
+
check_later << CheckLater.new(f, pv, tv)
|
|
230
|
+
elsif pv.is_a?(Bindable)
|
|
231
|
+
if pv._bound?
|
|
232
|
+
unless pv._value == tv
|
|
233
|
+
match_failed(f, "value does not match bound value #{pv._value.class}:#{pv._value.object_id} vs. #{tv.class}:#{tv.object_id}")
|
|
234
|
+
return false
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
pv._bind(tv)
|
|
238
|
+
end
|
|
239
|
+
else
|
|
240
|
+
if f.is_a?(RGen::ECore::EAttribute)
|
|
241
|
+
unless pv == tv
|
|
242
|
+
match_failed(f, "wrong attribute value")
|
|
243
|
+
return false
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
if pv.is_a?(Proxy)
|
|
247
|
+
unless pv._target.object_id == tv.object_id
|
|
248
|
+
match_failed(f, "wrong target object")
|
|
249
|
+
return false
|
|
250
|
+
end
|
|
251
|
+
else
|
|
252
|
+
unless both_nil_or_match(pv, tv, visited, check_later)
|
|
253
|
+
return false
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def match_many_bindable(bindable, test_values)
|
|
265
|
+
if bindable._bound?
|
|
266
|
+
bindable._value.each_with_index do |pv,i|
|
|
267
|
+
tv = test_values[i]
|
|
268
|
+
if f.is_a?(RGen::ECore::EAttribute)
|
|
269
|
+
unless pv == tv
|
|
270
|
+
match_failed(f, "wrong attribute value")
|
|
271
|
+
return false
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
unless both_nil_or_match(pv, tv, visited, check_later)
|
|
275
|
+
return false
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
else
|
|
280
|
+
bindable._bind(test_values.dup)
|
|
281
|
+
end
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def both_nil_or_match(pv, tv, visited, check_later)
|
|
286
|
+
(pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later))
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def match_failed(f, msg)
|
|
290
|
+
puts "match failed #{f&&f.eContainingClass.name}##{f&&f.name}: #{msg}" if @debug
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def num_pattern_variables(name)
|
|
294
|
+
prok = @patterns[name]
|
|
295
|
+
prok.arity - 1
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def evaluate_pattern(name, env, connection_points)
|
|
299
|
+
prok = @patterns[name]
|
|
300
|
+
raise "unknown pattern #{name}" unless prok
|
|
301
|
+
raise "wrong number of arguments, expected #{prok.arity-1} connection points)" \
|
|
302
|
+
unless connection_points.size == prok.arity-1
|
|
303
|
+
prok.call(env, *connection_points)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def disconnect_element(element)
|
|
307
|
+
return if element.nil?
|
|
308
|
+
all_structural_features(element).each do |f|
|
|
309
|
+
if f.many
|
|
310
|
+
element.setGeneric(f.name, [])
|
|
311
|
+
else
|
|
312
|
+
element.setGeneric(f.name, nil)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def all_structural_features(element)
|
|
318
|
+
@all_structural_features ||= {}
|
|
319
|
+
return @all_structural_features[element.class] if @all_structural_features[element.class]
|
|
320
|
+
@all_structural_features[element.class] =
|
|
321
|
+
element.class.ecore.eAllStructuralFeatures.reject{|f| f.derived}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
end
|
|
329
|
+
|