activefacts-orm 1.7.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd75d822b0249191ccb0a194683892d77c927002
4
+ data.tar.gz: 083c5a576d145726ca942393ad5827b4d5c57c8f
5
+ SHA512:
6
+ metadata.gz: b0591d5466cbbdc9e212c827210c9a75bb5d90cd0c7b786977cac789eec5cbdc34379473bc9400556d6ef97a426821d270f365b18e6ef02b6057b4d442966d01
7
+ data.tar.gz: 045f05c2a13de02a8ec099bcf3d75c8de04cdee00a62bacdca0ffc09093a8f7690da3efc9ff9bab1eb1c230020d5e8683c49e90e46325c125a2357c39fc71120
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
11
+ .DS_Store
12
+ *.rej
13
+ *.orig
14
+ *.diff
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ before_install: gem install bundler -v 1.10.0.rc
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ if ENV['PWD'] =~ %r{\A/Users/cjh/work/activefacts}
6
+ gem 'activefacts-api', path: '/Users/cjh/work/activefacts/api'
7
+ gem 'activefacts-metamodel', path: '/Users/cjh/work/activefacts/metamodel'
8
+ # gem 'activefacts-metamodel', git: 'git://github.com/cjheath/activefacts-metamodel.git'
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Clifford Heath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Activefacts::Orm
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/activefacts/orm`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'activefacts-orm'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install activefacts-orm
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/activefacts-orm.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activefacts/orm/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activefacts-orm"
8
+ spec.version = ActiveFacts::ORM::VERSION
9
+ spec.authors = ["Clifford Heath"]
10
+ spec.email = ["clifford.heath@gmail.com"]
11
+
12
+ spec.summary = %q{ORM format importer for the ActiveFacts fact modeling suite.}
13
+ spec.description = %q{ORM format importer for the ActiveFacts fact modeling suite.
14
+ Install the Natural ORM Architect from http://ormfoundation.org to produce input files.
15
+ }
16
+ spec.homepage = "http://github.com/cjheath/activefacts-orm"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10.a"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+
26
+ spec.add_runtime_dependency(%q<activefacts-metamodel>, [">= 1.7.0", "~> 1.7"])
27
+ spec.add_runtime_dependency(%q<nokogiri>)
28
+ end
@@ -0,0 +1,1636 @@
1
+ #
2
+ # ActiveFacts Vocabulary Input.
3
+ # Read a NORMA file into an ActiveFacts vocabulary
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # This code uses variables prefixed with x_ when they refer to Rexml nodes.
8
+ # Every node having an id="..." is indexed in @x_by_id[] hash before processing.
9
+ # As we build ActiveFacts objects to match, we index those in @by_id[].
10
+ # Both these hashes may be looked up by any of the ref="..." values in the file.
11
+ #
12
+ require 'nokogiri'
13
+ require 'activefacts/metamodel'
14
+
15
+ module Nokogiri
16
+ module XML
17
+ class Node
18
+ def elements
19
+ children.select{|n|
20
+ Nokogiri::XML::Element === n
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ module ActiveFacts
28
+ module Input
29
+ # Compile a NORMA (.orm) file to an ActiveFacts vocabulary.
30
+ # Invoke as
31
+ # afgen --<generator> <file>.orm
32
+ # This parser uses Rexml so it's very slow.
33
+ class ORM
34
+ module Gravity
35
+ %w{NW N NE W C E SW S SE}.each_with_index { |dir, i| const_set(dir, i) }
36
+ end
37
+
38
+ DataTypeMapping = {
39
+ "FixedLengthText" => "Char",
40
+ "VariableLengthText" => "String",
41
+ "LargeLengthText" => "Text",
42
+ "SignedIntegerNumeric" => "Signed Integer(32)",
43
+ "SignedSmallIntegerNumeric" => "Signed Integer(16)",
44
+ "SignedLargeIntegerNumeric" => "Signed Integer(64)",
45
+ "UnsignedIntegerNumeric" => "Unsigned Integer(32)",
46
+ "UnsignedTinyIntegerNumeric" => "Unsigned Integer(8)",
47
+ "UnsignedSmallIntegerNumeric" => "Unsigned Integer(16)",
48
+ "UnsignedLargeIntegerNumeric" => "Unsigned Integer(64)",
49
+ "AutoCounterNumeric" => "Auto Counter",
50
+ "FloatingPointNumeric" => "Real(64)",
51
+ "SinglePrecisionFloatingPointNumeric" => "Real(32)",
52
+ "DoublePrecisionFloatingPointNumeric" => "Real(32)",
53
+ "DecimalNumeric" => "Decimal",
54
+ "MoneyNumeric" => "Money",
55
+ "FixedLengthRawData" => "Blob",
56
+ "VariableLengthRawData" => "Blob",
57
+ "LargeLengthRawData" => "Blob",
58
+ "PictureRawData" => "Image",
59
+ "OleObjectRawData" => "Blob",
60
+ "AutoTimestampTemporal" => "Auto Time Stamp",
61
+ "TimeTemporal" => "Time",
62
+ "DateTemporal" => "Date",
63
+ "DateAndTimeTemporal" => "Date Time",
64
+ "TrueOrFalseLogical" => "Boolean",
65
+ "YesOrNoLogical" => "Boolean",
66
+ "RowIdOther" => "Guid",
67
+ "ObjectIdOther" => "Guid"
68
+ }
69
+ RESERVED_WORDS = %w{
70
+ and but each each either false if maybe no none not one or some that true where
71
+ }
72
+
73
+ private
74
+ def self.readfile(filename, *options)
75
+ if File.basename(filename, '.orm') == "-"
76
+ self.read(STDIN, "<standard input>", options)
77
+ else
78
+ File.open(filename) {|file|
79
+ self.read(file, filename, *options)
80
+ }
81
+ end
82
+ end
83
+
84
+ def self.read(file, filename = "stdin", *options)
85
+ ORM.new(file, filename, *options).read
86
+ end
87
+
88
+ def initialize(file, filename = "stdin", *options)
89
+ @file = file
90
+ @filename = filename
91
+ @options = options
92
+ end
93
+
94
+ public
95
+ def read #:nodoc:
96
+ begin
97
+ @document = Nokogiri::XML(@file)
98
+ rescue => e
99
+ puts "Failed to parse XML in #{@filename}: #{e.inspect}"
100
+ end
101
+
102
+ # Find the Vocabulary and do some setup:
103
+ root = @document.root
104
+ #p((root.methods-0.methods).sort.grep(/name/))
105
+ if root.name == "ORM2" && root.namespace.prefix == "ormRoot"
106
+ x_models = root.xpath('orm:ORMModel')
107
+ throw "No vocabulary found" unless x_models.size == 1
108
+ @x_model = x_models[0]
109
+ elsif root.name == "ORMModel"
110
+ p @document.children.map(&:name)
111
+ @x_model = @document.children[0]
112
+ else
113
+ throw "NORMA model not found in #{@filename}"
114
+ end
115
+
116
+ read_vocabulary
117
+ @vocabulary
118
+ end
119
+
120
+ private
121
+
122
+ def read_vocabulary
123
+ @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
124
+ vocabulary_name = @x_model['Name']
125
+ @vocabulary = @constellation.Vocabulary(vocabulary_name)
126
+
127
+ # Find all elements having an "id" attribute and index them
128
+ x_identified = @x_model.xpath(".//*[@id]")
129
+ @x_by_id = x_identified.inject({}){|h, x|
130
+ id = x['id']
131
+ h[id] = x
132
+ h
133
+ }
134
+
135
+ # Everything we build will be indexed here:
136
+ @by_id = {}
137
+
138
+ list_subtypes
139
+ read_entity_types
140
+ read_value_types
141
+ read_fact_types
142
+ read_nested_types
143
+ read_subtypes
144
+ read_roles
145
+ complete_nested_types
146
+ read_constraints
147
+ read_instances if @options.include?("instances")
148
+ read_diagrams if @options.include?("diagrams")
149
+ end
150
+
151
+ def id_of(x)
152
+ x['id'][1..-1]
153
+ end
154
+
155
+ def read_entity_types
156
+ # get and process all the entity types:
157
+ entity_types = []
158
+ x_entity_types = @x_model.xpath("orm:Objects/orm:EntityType")
159
+ x_entity_types.each do |x|
160
+ id = x['id']
161
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
162
+ name = nil if name.size == 0
163
+ entity_type =
164
+ @by_id[id] =
165
+ trace :orm, "Asserting new EntityType #{name.inspect}" do
166
+ @vocabulary.valid_entity_type_name(name) ||
167
+ @constellation.EntityType(@vocabulary, name, :concept => id_of(x))
168
+ end
169
+ entity_types << entity_type
170
+ independent = x['IsIndependent']
171
+ entity_type.is_independent = true if independent && independent == 'true'
172
+ personal = x['IsPersonal']
173
+ entity_type.pronoun = 'personal' if personal && personal == 'true'
174
+ # x_pref = x.xpath("orm:PreferredIdentifier")[0]
175
+ # if x_pref
176
+ # pi_id = x_pref['ref']
177
+ # @pref_id_for[pi_id] = x
178
+ # end
179
+ end
180
+ end
181
+
182
+ def read_value_types
183
+ # Now the value types:
184
+ value_types = []
185
+ x_value_types = @x_model.xpath("orm:Objects/orm:ValueType")
186
+ @value_type_id_read = {}
187
+ x_value_types.each{|x|
188
+ next if x['IsImplicitBooleanValue']
189
+ value_types << read_value_type(x)
190
+ }
191
+ end
192
+
193
+ def read_value_type x
194
+ id = x['id']
195
+ return if @value_type_id_read[id] # Don't read the same value type twice
196
+ @value_type_id_read[id] = true
197
+
198
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
199
+ name = nil if name.size == 0
200
+
201
+ cdt = x.xpath('orm:ConceptualDataType')[0]
202
+ scale = cdt['Scale']
203
+ scale = scale != "" && scale.to_i
204
+ length = cdt['Length']
205
+ length = length != "" && length.to_i
206
+ length = nil if length <= 0
207
+ base_type = @x_by_id[cdt['ref']]
208
+ type_name = "#{base_type.name}"
209
+ type_name.sub!(/^orm:/,'')
210
+
211
+ type_name.sub!(/DataType\Z/,'')
212
+ if t = DataTypeMapping[type_name]
213
+ existing = @constellation.ObjectType[[@vocabulary.identifying_role_values, t]]
214
+ if !existing || existing.is_a?(ActiveFacts::Metamodel::ValueType)
215
+ # There's no type conflict, so use the mapped name.
216
+ type_name = t
217
+ end
218
+ end
219
+ trace :orm, "Using #{type_name.inspect} as supertype for new #{name}"
220
+ if !length and type_name =~ /\(([0-9]+)\)/
221
+ length = $1.to_i
222
+ end
223
+ type_name = type_name.sub(/\(([0-9]*)\)/,'')
224
+
225
+ subtype_roles = x.xpath("orm:PlayedRoles/orm:SubtypeMetaRole")
226
+ value_super_type = nil
227
+ if !subtype_roles.empty?
228
+ subtype_role_id = subtype_roles[0]['ref']
229
+ subtype_role = @x_by_id[subtype_role_id]
230
+ subtyping_fact_roles = subtype_role.parent
231
+ supertype_id = subtyping_fact_roles.xpath("orm:SupertypeMetaRole/orm:RolePlayer")[0]['ref']
232
+ x_supertype = @x_by_id[supertype_id]
233
+ read_value_type x_supertype unless @value_type_id_read[supertype_id]
234
+ supertype = @by_id[supertype_id]
235
+ supertype_name = x_supertype['Name']
236
+ raise "Supertype of #{name} is post-defined but recursiving processing failed" unless supertype
237
+ raise "Supertype #{supertype_name} of #{name} is not a value type" unless supertype.kind_of? ActiveFacts::Metamodel::ValueType
238
+ trace :orm, "Asserting new ValueType #{supertype_name.inspect} for supertype" do
239
+ value_super_type =
240
+ @vocabulary.valid_value_type_name(supertype_name) ||
241
+ @constellation.ValueType(@vocabulary, supertype_name, :concept => id_of(x_supertype))
242
+ end
243
+ else
244
+ # REVISIT: Need to handle standard types better here:
245
+ value_super_type =
246
+ if type_name != name
247
+ trace :orm, "Asserting new ValueType #{type_name.inspect} for supertype" do
248
+ @vocabulary.valid_value_type_name(type_name) ||
249
+ @constellation.ValueType(@vocabulary.identifying_role_values, type_name, :concept => :new)
250
+ end
251
+ else
252
+ nil
253
+ end
254
+ end
255
+
256
+ vt =
257
+ trace :orm, "Asserting new ValueType #{name.inspect}" do
258
+ @by_id[id] =
259
+ @vocabulary.valid_value_type_name(name) ||
260
+ @constellation.ValueType(@vocabulary.identifying_role_values, name, :concept => id_of(x))
261
+ end
262
+ vt.supertype = value_super_type
263
+ vt.length = length if length
264
+ vt.scale = scale if scale && scale != 0
265
+ independent = x['IsIndependent']
266
+ vt.is_independent = true if independent && independent == 'true'
267
+ personal = x['IsPersonal']
268
+ vt.pronoun = 'personal' if personal && personal == 'true'
269
+
270
+ x_vr = x.xpath("orm:ValueRestriction/orm:ValueConstraint")
271
+ x_vr.each{|vr|
272
+ x_ranges = vr.xpath("orm:ValueRanges/orm:ValueRange")
273
+ next if x_ranges.size == 0
274
+ vt.value_constraint = @by_id[vr['id']] = @constellation.ValueConstraint(id_of(vr))
275
+ x_ranges.each{|x_range|
276
+ v_range = value_range(x_range)
277
+ ar = @constellation.AllowedRange(vt.value_constraint, v_range)
278
+ }
279
+ }
280
+ vt
281
+ end
282
+
283
+ def assert_value(val, string)
284
+ if val
285
+ @constellation.Value(val.to_s, string, nil)
286
+ else
287
+ nil
288
+ end
289
+ end
290
+
291
+ def value_range(x_range)
292
+ min = x_range['MinValue']
293
+ max = x_range['MaxValue']
294
+
295
+ strings = is_literal_string(min) || is_literal_string(max)
296
+ # ValueRange takes a minimum and/or a maximum Bound, each takes value and whether inclusive
297
+ @constellation.ValueRange(
298
+ min.length > 0 ? @constellation.Bound(assert_value(min, strings), true) : nil,
299
+ max.length > 0 ? @constellation.Bound(assert_value(max, strings), true) : nil)
300
+ end
301
+
302
+ def read_fact_types
303
+ # Handle the fact types:
304
+ facts = []
305
+ @x_facts = @x_model.xpath("orm:Facts/orm:Fact")
306
+ trace :orm, "Reading fact types" do
307
+ @x_facts.each{|x|
308
+ id = x['id']
309
+ name = x['Name'] || x['_Name']
310
+ name = "<unnamed>" if !name
311
+ name = "" if !name || name.size == 0
312
+ # Note that the new metamodel doesn't have a name for a facttype unless it's objectified
313
+
314
+ trace :orm, "FactType #{name || id}"
315
+ facts << @by_id[id] = fact_type = @constellation.FactType(id_of(x))
316
+ }
317
+ end
318
+ end
319
+
320
+ def list_subtypes
321
+ @x_subtypes = @x_model.xpath("orm:Facts/orm:SubtypeFact")
322
+ if @document.namespaces['xmlns:oialtocdb']
323
+ oialtocdb = @document.xpath("ormRoot:ORM2/oialtocdb:MappingCustomization")
324
+ @x_mappings = oialtocdb.xpath(".//oialtocdb:AssimilationMappings/oialtocdb:AssimilationMapping/oialtocdb:FactType")
325
+ else
326
+ @x_mappings = []
327
+ end
328
+ end
329
+
330
+ def read_subtypes
331
+ # Handle the subtype fact types:
332
+ facts = []
333
+
334
+ trace :orm, "Reading sub-types" do
335
+ @x_subtypes.each{|x|
336
+ id = x['id']
337
+ name = (x['Name'] || x['_Name'] || '').gsub(/\s+/,' ').gsub(/-/,'_').strip
338
+ name = nil if name.size == 0
339
+ trace :orm, "FactType #{name || id}"
340
+
341
+ x_subtype_role = x.xpath('orm:FactRoles/orm:SubtypeMetaRole')[0]
342
+ subtype_role_id = x_subtype_role['id']
343
+ subtype_id = x_subtype_role.xpath('orm:RolePlayer')[0]['ref']
344
+ subtype = @by_id[subtype_id]
345
+ # REVISIT: Provide a way in the metamodel of handling Partition, (and mapping choices that vary for each supertype?)
346
+
347
+ x_supertype_role = x.xpath('orm:FactRoles/orm:SupertypeMetaRole')[0]
348
+ supertype_role_id = x_supertype_role['id']
349
+ supertype_id = x_supertype_role.xpath('orm:RolePlayer')[0]['ref']
350
+ supertype = @by_id[supertype_id]
351
+
352
+ throw "For Subtype fact #{name}, the supertype #{supertype_id} was not found" if !supertype
353
+ throw "For Subtype fact #{name}, the subtype #{subtype_id} was not found" if !subtype
354
+ trace :orm, "#{subtype.name} is a subtype of #{supertype.name}"
355
+
356
+ # We already handled ValueType subtyping:
357
+ next if subtype.kind_of? ActiveFacts::Metamodel::ValueType or
358
+ supertype.kind_of? ActiveFacts::Metamodel::ValueType
359
+
360
+ inheritance_fact = @constellation.TypeInheritance(subtype, supertype, :concept => id_of(x))
361
+ if x["IsPrimary"] == "true" or # Old way
362
+ x["PreferredIdentificationPath"] == "true" # Newer
363
+ trace :orm, "#{supertype.name} is primary supertype of #{subtype.name}"
364
+ inheritance_fact.provides_identification = true
365
+ end
366
+ mapping = @x_mappings.detect{ |m| m['ref'] == id }
367
+ mapping_choice = mapping ? mapping.parent['AbsorptionChoice'] : 'Absorbed'
368
+ inheritance_fact.assimilation = mapping_choice.downcase.sub(/partition/, 'partitioned') if mapping_choice != 'Absorbed'
369
+ facts << @by_id[id] = inheritance_fact
370
+
371
+ # Create the new Roles so we can find constraints on them:
372
+ subtype_role = @by_id[subtype_role_id] = @constellation.Role(inheritance_fact, 0, :object_type => subtype, :concept => id_of(x_subtype_role))
373
+ supertype_role = @by_id[supertype_role_id] = @constellation.Role(inheritance_fact, 1, :object_type => supertype, :concept => id_of(x_supertype_role))
374
+
375
+ # Create readings, so constraints can be verbalised for example:
376
+ rs = @constellation.RoleSequence(:new)
377
+ @constellation.RoleRef(rs, 0, :role => subtype_role)
378
+ @constellation.RoleRef(rs, 1, :role => supertype_role)
379
+ @constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :text => "{0} is a kind of {1}", :is_negative => false)
380
+ @constellation.Reading(inheritance_fact, 1, :role_sequence => rs, :text => "{0} is a subtype of {1}", :is_negative => false)
381
+
382
+ rs2 = @constellation.RoleSequence(:new)
383
+ @constellation.RoleRef(rs2, 0, :role => supertype_role)
384
+ @constellation.RoleRef(rs2, 1, :role => subtype_role)
385
+ n = 'aeioh'.include?(subtype_role.object_type.name.downcase[0]) ? 1 : 0
386
+ @constellation.Reading(inheritance_fact, 2+n, :role_sequence => rs2, :text => "{0} is a {1}", :is_negative => false)
387
+ @constellation.Reading(inheritance_fact, 3-n, :role_sequence => rs2, :text => "{0} is an {1}", :is_negative => false)
388
+ }
389
+ end
390
+ end
391
+
392
+ def read_nested_types
393
+ # Process NestedTypes, but ignore ones having a NestedPredicate with IsImplied="true"
394
+ # We'll ignore the fact roles (and constraints) that implied objectifications have.
395
+ # This happens for all ternaries and higher order facts
396
+ @nested_types = []
397
+ x_nested_types = @x_model.xpath("orm:Objects/orm:ObjectifiedType")
398
+ trace :orm, "Reading objectified types" do
399
+ x_nested_types.each{|x|
400
+ id = x['id']
401
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
402
+ name = nil if name.size == 0
403
+
404
+ x_fact_type = x.xpath('orm:NestedPredicate')[0]
405
+ is_implied = x_fact_type['IsImplied'] == "true"
406
+
407
+ fact_id = x_fact_type['ref']
408
+ fact_type = @by_id[fact_id]
409
+ next unless fact_type # "Nested fact #{fact_id} not found; objectification of a derived fact type?"
410
+
411
+ trace :orm, "NestedType #{name} is #{id}, nests #{fact_type.concept.guid}"
412
+ @nested_types <<
413
+ @by_id[id] =
414
+ nested_type = @vocabulary.valid_entity_type_name(name) ||
415
+ @constellation.EntityType(@vocabulary, name, :concept => id_of(x))
416
+ independent = x['IsIndependent']
417
+ nested_type.is_independent = true if independent && independent == 'true' && !is_implied
418
+ nested_type.concept.implication_rule = 'objectification' if is_implied
419
+ nested_type.fact_type = fact_type
420
+ }
421
+ end
422
+ end
423
+
424
+ def complete_nested_types
425
+ @nested_types.each do |nested_type|
426
+ # Create the phantom roles here. These will be used later when we create objectification steps,
427
+ # but for now there's nothing we import from NORMA which requires objectification steps.
428
+ # Consequently there's no need to index them against NORMA's phantom roles.
429
+ nested_type.create_implicit_fact_types
430
+ end
431
+ end
432
+
433
+ def read_roles
434
+ trace :orm, "Reading roles and readings" do
435
+ @x_facts.each{|x|
436
+ id = x['id']
437
+ fact_type = @by_id[id]
438
+ fact_name = x['Name'] || x['_Name'] || ''
439
+ #fact_name.gsub!(/\s/,'')
440
+ fact_name = nil if fact_name == ''
441
+
442
+ x_fact_roles = x.xpath('orm:FactRoles/*')
443
+
444
+ # Deal with FactRoles (Roles):
445
+ trace :orm, "Reading fact roles" do
446
+ x_fact_roles.each{|x|
447
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
448
+ name = nil if name.size == 0
449
+
450
+ # _IsMandatory = x['_IsMandatory']
451
+ # _Multiplicity = x['_Multiplicity]
452
+ id = x['id']
453
+ rp = x.xpath('orm:RolePlayer')[0]
454
+ raise "Invalid ORM file; fact has missing player (RolePlayer id=#{id})" unless rp
455
+ ref = rp['ref']
456
+
457
+ # Find the object_type that plays the role:
458
+ object_type = @by_id[ref]
459
+
460
+ # Skip implicit roles added by NORMA to make unaries into binaries.
461
+ # This would make constraints over the deleted roles impossible,
462
+ # so as a SPECIAL CASE we index the unary role by the id of the
463
+ # implicit role. That means care is needed when handling unary FTs.
464
+ if (ox = @x_by_id[ref]) && ox['IsImplicitBooleanValue']
465
+ x_other_role = x.parent.xpath('orm:Role').reject{|x_role|
466
+ x_role == x
467
+ }[0]
468
+ other_role_id = x_other_role["id"]
469
+ other_role = @by_id[other_role_id]
470
+ trace :orm, "Indexing unary FT role #{other_role_id} by implicit boolean role #{id}"
471
+ @by_id[id] = other_role
472
+
473
+ # The role name of the ignored role is the one that applies:
474
+ role_name = x['Name']
475
+ other_role.role_name = role_name if role_name && role_name != ''
476
+
477
+ @by_id.delete(ref) # and de-index it from our list
478
+ next
479
+ end
480
+ if !object_type
481
+ throw "RolePlayer for '#{name}' #{ref} in fact type #{x.parent.parent['_Name']} was not found"
482
+ end
483
+
484
+ trace :orm, "#{@vocabulary.name}, RoleName=#{x['Name'].inspect} played by object_type=#{object_type.name}"
485
+ throw "Role is played by #{object_type.class} not ObjectType" if !(@constellation.vocabulary.object_type(:ObjectType) === object_type)
486
+
487
+ trace :orm, "Creating role #{name} nr#{fact_type.all_role.size} of #{fact_type.concept.guid} played by #{object_type.name}"
488
+
489
+ role = @by_id[id] = @constellation.Role(fact_type, fact_type.all_role.size, :object_type => object_type, :concept => id_of(x))
490
+ role.role_name = name if name && name != object_type.name
491
+ trace :orm, "Fact #{fact_name} (id #{fact_type.concept.guid}) role #{x['Name']} is played by #{object_type.name}, role is #{role.concept.guid}"
492
+
493
+ x_vr = x.xpath("orm:ValueRestriction/orm:RoleValueConstraint")
494
+ x_vr.each{|vr|
495
+ x_ranges = vr.xpath("orm:ValueRanges/orm:ValueRange")
496
+ next if x_ranges.size == 0
497
+ role.role_value_constraint = @by_id[vr['id']] = @constellation.ValueConstraint(id_of(vr))
498
+ x_ranges.each{|x_range|
499
+ v_range = value_range(x_range)
500
+ ar = @constellation.AllowedRange(role.role_value_constraint, v_range)
501
+ }
502
+ }
503
+
504
+ trace :orm, "Adding Role #{role.role_name || role.object_type.name} to #{fact_type.describe}"
505
+ #fact_type.add_role(role)
506
+ trace :orm, "Role #{role} is #{id}"
507
+ }
508
+ end
509
+
510
+ # Deal with Readings:
511
+ trace :orm, "Reading fact readings" do
512
+ x_reading_orders = x.xpath('orm:ReadingOrders/*')
513
+ x_reading_orders.each{|x|
514
+ x_role_sequence = x.xpath('orm:RoleSequence/*')
515
+ x_readings = x.xpath('orm:Readings/orm:Reading/orm:Data')
516
+
517
+ # Build an array of the Roles needed:
518
+ role_array = x_role_sequence.map{|x| @by_id[x['ref']] }
519
+
520
+ trace :orm, "Reading #{x_readings.map(&:text).inspect}"
521
+ role_sequence = get_role_sequence(role_array)
522
+
523
+ #role_sequence.all_role_ref.each_with_index{|rr, i|
524
+ # # REVISIT: rr.leading_adjective = ...; Add adjectives here
525
+ # }
526
+
527
+ x_readings.each_with_index{|x, i|
528
+ reading = @constellation.Reading(fact_type, fact_type.all_reading.size, :is_negative => false)
529
+ reading.role_sequence = role_sequence
530
+ # REVISIT: The downcase here only needs to be the initial letter of each word, but be safe:
531
+ reading.text = extract_adjectives(x.text, role_sequence)
532
+ }
533
+ }
534
+ end
535
+ }
536
+ end
537
+ end
538
+
539
+ def extract_adjectives(text, role_sequence)
540
+ all_role_refs = role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}
541
+ (0...all_role_refs.size).each{|i|
542
+ role_ref = all_role_refs[i]
543
+ role = role_ref.role
544
+
545
+ word = '\b[A-Za-z_][A-Za-z0-9_]+\b'
546
+ leading_adjectives_re = "#{word}-+(?: +#{word})*"
547
+ trailing_adjectives_re = "(?:#{word} +)*-+#{word}"
548
+ role_with_adjectives_re =
549
+ %r| ?(#{leading_adjectives_re})? *\{#{i}\} *(#{trailing_adjectives_re})? ?|
550
+
551
+ # A hyphenated pre-bound reading looks like this:
552
+ # <orm:Data>{0} has pre-- bound {1}</orm:Data>
553
+
554
+ text.gsub!(role_with_adjectives_re) {
555
+ # REVISIT: Don't want to strip all spaces here any more:
556
+ #puts "text=#{text.inspect}, la=#{$1.inspect}, ta=#{$2.inspect}" if $1 || $2
557
+ la = ($1||'').gsub(/\s+/,' ') # Strip duplicate spaces
558
+ ta = ($2||'').gsub(/\s+/,' ')
559
+ # When we have "aaa-bbb" we want "aaa bbb"
560
+ # When we have "aaa- bbb" we want "aaa bbb"
561
+ # When we have "aaa-- bbb" we want "aaa-bbb"
562
+ la = la.sub(/(-)?- ?/,'\1').strip
563
+ ta = ta.sub(/ ?(-)?-/,'\1').strip
564
+ #puts "Setting leading adj #{la.inspect} from #{text.inspect} for #{role_ref.role.object_type.name}" if la != ""
565
+ # REVISIT: Dunno what's up here, but removing the "if" test makes this chuck exceptions:
566
+ role_ref.leading_adjective = la if la != ""
567
+ role_ref.trailing_adjective = ta if ta != ""
568
+
569
+ #puts "Reading '#{text}' has role #{i} adjectives '#{la}' '#{ta}'" if la != "" || ta != ""
570
+
571
+ " {#{i}} "
572
+ }
573
+ }
574
+ text.sub!(/\s\s*/, ' ') # Compress extra spaces
575
+ text.strip!
576
+ text.downcase! # Check for reserved words and object type names *after* downcasing
577
+ elided = ''
578
+ text.gsub!(/( |[a-z]+(-[a-z]+)+|-?\b[A-Za-z_][A-Za-z0-9_]*\b-?|\{\d\})|./) do |w|
579
+ case w
580
+ when /[A-Za-z]/
581
+ if RESERVED_WORDS.include?(w)
582
+ $stderr.puts "Masking reserved word '#{w}' in reading '#{text}'"
583
+ next "_#{w}"
584
+ elsif @constellation.ObjectType[[[@vocabulary.name], w]]
585
+ $stderr.puts "Masking object type name '#{w}' in reading '#{text}'"
586
+ next "_#{w}"
587
+ elsif all_role_refs.detect{|rr| rr.role.role_name == w}
588
+ $stderr.puts "Masking role name '#{w}' in reading '#{text}'"
589
+ next "_#{w}"
590
+ end
591
+ next w
592
+ when /\{\d\}/
593
+ next w
594
+ when / /
595
+ next w
596
+ else
597
+ elided << w
598
+ next ''
599
+ end
600
+ end
601
+ $stderr.puts "Elided illegal characters '#{elided}' from reading #{text.inspect}" unless elided.empty?
602
+ text
603
+ end
604
+
605
+ def get_role_sequence(role_array)
606
+ # puts "Getting RoleSequence [#{role_array.map{|r| "#{r.object_type.name} (role #{r.concept.guid})" }*", "}]"
607
+
608
+ # Look for an existing RoleSequence
609
+ # REVISIT: This searches all role sequences. Perhaps we could narrow it down first instead?
610
+ role_sequence = @constellation.RoleSequence.values.detect{|c|
611
+ #puts "Checking RoleSequence [#{c.all_role_ref.map{|rr| rr.role.object_type.name}*", "}]"
612
+ role_array == c.all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.role }
613
+ }
614
+ # puts "Found matching RoleSequence!" if role_sequence
615
+ return role_sequence if role_sequence
616
+
617
+ # Make a new RoleSequence:
618
+ role_sequence = @constellation.RoleSequence(:new) unless role_sequence
619
+ role_array.each_with_index do |r, i|
620
+ role_ref = @constellation.RoleRef(role_sequence, i)
621
+ role_ref.role = r
622
+ end
623
+
624
+ role_sequence
625
+ end
626
+
627
+ def map_roles(x_roles, why = nil)
628
+ role_array = x_roles.map do |x|
629
+ id = x['ref']
630
+ role = @by_id[id]
631
+ if (why && !role)
632
+ # We didn't make Implied objects, so some constraints are unconnectable
633
+ x_role = @x_by_id[id]
634
+ x_player = x_role.xpath('orm:RolePlayer')[0]
635
+ x_object = @x_by_id[x_player['ref']]
636
+ x_nests = nil
637
+ if (x_object.name.to_s == 'ObjectifiedType')
638
+ x_nests = x_object.xpath('orm:NestedPredicate')[0]
639
+ implied = x_nests['IsImplied']
640
+ # x_fact is the fact of which the role player is an objectification, not the fact this role belongs to
641
+ x_fact = @x_by_id[x_nests['ref']]
642
+ end
643
+
644
+ # This might have been a role of an ImpliedFact, which makes it safe to ignore.
645
+ next if 'ImpliedFact' == x_role.parent.parent.name
646
+
647
+ # Talk about why this wasn't found - this shouldn't happen.
648
+ if (!x_nests || !implied)
649
+ #puts "="*60
650
+ # We skip creating TypeInheritance implied fact types for ValueType inheritance
651
+ return nil if x_role.name = 'orm:SubtypeMetaRole' or x_role.name = 'orm:SupertypeMetaRole'
652
+ raise "Skipping #{why}, #{x_role.name} #{id} not found"
653
+
654
+ if (x_nests)
655
+ puts "Role is on #{implied ? "implied " : ""}objectification #{x_object}"
656
+ puts "which objectifies #{x_fact}"
657
+ end
658
+ puts x_object.to_s
659
+ end
660
+ end
661
+ role
662
+ end
663
+ return nil if role_array.include?(nil)
664
+
665
+ get_role_sequence(role_array)
666
+ end
667
+
668
+ def read_constraints
669
+ @constraints_by_rs = {}
670
+
671
+ read_mandatory_constraints
672
+ read_uniqueness_constraints
673
+ read_exclusion_constraints
674
+ read_subset_constraints
675
+ read_ring_constraints
676
+ read_equality_constraints
677
+ read_frequency_constraints
678
+ read_residual_mandatory_constraints
679
+ end
680
+
681
+ def read_mandatory_constraints
682
+ x_mandatory_constraints = @x_model.xpath("orm:Constraints/orm:MandatoryConstraint")
683
+ @mandatory_constraints_by_rs = {}
684
+ @mandatory_constraint_rs_by_id = {}
685
+ trace :orm, "Scanning mandatory constraints" do
686
+ x_mandatory_constraints.each{|x|
687
+ name = x["Name"] || ''
688
+ name = nil if name.size == 0
689
+
690
+ # As of Feb 2008, all NORMA ValueTypes have an implied mandatory constraint.
691
+ next if x.xpath("orm:ImpliedByObjectType").size > 0
692
+
693
+ x_roles = x.xpath("orm:RoleSequence/orm:Role")
694
+ role_sequence = map_roles(x_roles, "mandatory constraint #{name}")
695
+ next if !role_sequence
696
+
697
+ trace :orm, "New MC #{x['Name']} over #{role_sequence.describe}"
698
+ @mandatory_constraints_by_rs[role_sequence] = x
699
+ @mandatory_constraint_rs_by_id[x['id']] = role_sequence
700
+ }
701
+ end
702
+ end
703
+
704
+ # Mandatory constraints that didn't get merged with an exclusion constraint or a uniqueness constraint are simple mandatories
705
+ def read_residual_mandatory_constraints
706
+ trace :orm, "Processing non-absorbed mandatory constraints" do
707
+ @mandatory_constraints_by_rs.each { |role_sequence, x|
708
+ id = x['id']
709
+ # Create a simply-mandatory PresenceConstraint for each mandatory constraint
710
+ name = x["Name"] || ''
711
+ name = nil if name.size == 0
712
+ #puts "Residual Mandatory #{name}: #{role_sequence.to_s}"
713
+
714
+ if (players = role_sequence.all_role_ref.map{|rr| rr.role.object_type}).uniq.size > 1
715
+ join_over, = *ActiveFacts::Metamodel.plays_over(role_sequence.all_role_ref.map{|rr| rr.role}, :proximate)
716
+ raise "Mandatory join constraint #{name} has incompatible players #{players.map{|o| o.name}.inspect}" unless join_over
717
+ if players.detect{|p| p != join_over}
718
+ trace :query, "subtyping step simple mandatory constraint #{name} over #{join_over.name}"
719
+ players.each_with_index do |player, i|
720
+ next if player != join_over
721
+ # REVISIT: We don't need to make a subtyping step here (from join_over to player)
722
+ end
723
+ end
724
+ end
725
+
726
+ pc = @constellation.PresenceConstraint(id_of(x))
727
+ pc.vocabulary = @vocabulary
728
+ pc.name = name
729
+ pc.role_sequence = role_sequence
730
+ pc.is_mandatory = true
731
+ pc.min_frequency = 1
732
+ pc.max_frequency = nil
733
+ pc.is_preferred_identifier = false
734
+
735
+ (@constraints_by_rs[role_sequence] ||= []) << pc
736
+ @by_id[id] = pc
737
+ }
738
+ end
739
+ end
740
+
741
+ def read_uniqueness_constraints
742
+ x_uniqueness_constraints = @x_model.xpath("orm:Constraints/orm:UniquenessConstraint")
743
+ trace :orm, "Reading uniqueness constraints" do
744
+ x_uniqueness_constraints.each{|x|
745
+ name = x["Name"] || ''
746
+ name = nil if name.size == 0
747
+ uc_id = x["id"]
748
+ x_pi = x.xpath("orm:PreferredIdentifierFor")[0]
749
+ pi = x_pi ? @by_id[eref = x_pi['ref']] : nil
750
+
751
+ # Skip uniqueness constraints on implied object_types
752
+ next if x_pi && !pi
753
+
754
+ # Get the RoleSequence:
755
+ x_roles = x.xpath("orm:RoleSequence/orm:Role")
756
+ next if x_roles.size == 0
757
+ role_sequence = map_roles(x_roles, "uniqueness constraint #{name}")
758
+ next if !role_sequence
759
+ #puts "uc: #{role_sequence.all_role_ref.map{|rr|rr.role.fact_type.default_reading}*', '}"
760
+
761
+ # Check for a query
762
+ if (fact_types = role_sequence.all_role_ref.map{|rr| rr.role.fact_type}).uniq.size > 1
763
+ join_over, = *ActiveFacts::Metamodel.plays_over(role_sequence.all_role_ref.map{|rr| rr.role}, :counterpart)
764
+
765
+ players = role_sequence.all_role_ref.map{|rr| rr.role.object_type.name}.uniq
766
+ unless join_over
767
+ if x.xpath("orm:RoleSequence/orm:JoinRule").size > 0
768
+ $stderr.puts "Ignoring Join #{name} because its query is not understood"
769
+ next
770
+ end
771
+ raise "Uniqueness join constraint #{name} has incompatible players #{players.inspect}"
772
+ end
773
+ subtyping = players.size > 1 ? 'subtyping ' : ''
774
+ # REVISIT: Create the Query, the Variable for join_over, and steps from each role_ref to join_over
775
+ trace :query, "#{subtyping}join uniqueness constraint over #{join_over.name} in #{fact_types.map(&:default_reading)*', '}"
776
+ end
777
+
778
+ # There is an implicit uniqueness constraint when any object plays a unary. Skip it.
779
+ if (x_roles.size == 1 &&
780
+ (id = x_roles[0]['ref']) &&
781
+ (x_role = @x_by_id[id]) &&
782
+ (nodes = x_role.parent.elements).size == 2 &&
783
+ (sibling = nodes[1]) &&
784
+ (ib_id = sibling.elements[0]['ref']) &&
785
+ (ib = @x_by_id[ib_id]) &&
786
+ ib['IsImplicitBooleanValue'])
787
+ unary_identifier = true
788
+ end
789
+
790
+ mc_id = nil
791
+
792
+ if (mc = @mandatory_constraints_by_rs[role_sequence])
793
+ # Remove absorbed mandatory constraints, leaving residual ones.
794
+ trace :orm, "Absorbing MC #{mc['Name']} over #{role_sequence.describe}"
795
+ @mandatory_constraints_by_rs.delete(role_sequence)
796
+ mc_id = mc['id']
797
+ @mandatory_constraint_rs_by_id.delete(mc['id'])
798
+ elsif (fts = role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq).size == 1 and
799
+ fts[0].entity_type
800
+ # this uniqueness constraint is an internal UC on an objectified fact type,
801
+ # so the covered roles are always mandatory (wrt the OFT)
802
+ # That is, the phantom roles are mandatory, even if the visible roles are not.
803
+ mc = true
804
+ else
805
+ trace :orm, "No MC to absorb over #{role_sequence.describe}"
806
+ end
807
+
808
+ # A TypeInheritance fact type has a uniqueness constraint on each role.
809
+ # If this UC is on the supertype and identifies the subtype, it's preferred:
810
+ is_supertype_constraint =
811
+ (rr = role_sequence.all_role_ref.single) &&
812
+ (role = rr.role) &&
813
+ (fact_type = role.fact_type) &&
814
+ fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
815
+ role.object_type == fact_type.supertype &&
816
+ fact_type.provides_identification
817
+
818
+ pc = @constellation.PresenceConstraint(id_of(x))
819
+ pc.vocabulary = @vocabulary
820
+ pc.name = name
821
+ pc.role_sequence = role_sequence
822
+ pc.is_mandatory = true if mc
823
+ pc.min_frequency = mc ? 1 : 0
824
+ pc.max_frequency = 1
825
+ pc.is_preferred_identifier = true if pi || unary_identifier || is_supertype_constraint
826
+ trace :orm, "#{name} covers #{role_sequence.describe} has min=#{pc.min_frequency}, max=1, preferred=#{pc.is_preferred_identifier.inspect}"
827
+
828
+ trace :orm, role_sequence.all_role_ref.to_a[0].role.fact_type.describe + " is subject to " + pc.describe if role_sequence.all_role_ref.all?{|r| r.role.fact_type.is_a? ActiveFacts::Metamodel::TypeInheritance }
829
+
830
+ (@constraints_by_rs[role_sequence] ||= []) << pc
831
+ @by_id[uc_id] = pc
832
+ @by_id[mc_id] = pc if mc_id
833
+ }
834
+ end
835
+ end
836
+
837
+ def subtype_step(query, ti)
838
+ subtype_node = query.all_variable.detect{|jn| jn.object_type == ti.subtype } ||
839
+ @constellation.Variable(query, query.all_variable.size, :object_type => ti.subtype)
840
+ supertype_node = query.all_variable.detect{|jn| jn.object_type == ti.supertype } ||
841
+ @constellation.Variable(query, query.all_variable.size, :object_type => ti.supertype)
842
+ step = @constellation.Step(:guid => :new, :fact_type => ti)
843
+ rs = @constellation.RoleSequence(:new)
844
+ @constellation.RoleRef(rs, 0, :role => ti.subtype_role)
845
+ sub_play = @constellation.Play(:step => step, :variable => subtype_node, :role => ti.subtype_role)
846
+ @constellation.RoleRef(rs, 1, :role => ti.supertype_role)
847
+ sup_play = @constellation.Play(:step => step, :variable => supertype_node, :role => ti.supertype_role, :is_input => true)
848
+ trace :query, "New subtyping step #{step.describe}"
849
+ step
850
+ end
851
+
852
+ # Make as many steps as it takes to get from subtype to supertype
853
+ def subtype_steps(query, subtype, supertype)
854
+ primary_ti = nil
855
+ other_ti = nil
856
+ subtype.all_type_inheritance_as_subtype.each do |ti|
857
+ next unless ti.supertype.supertypes_transitive.include? supertype
858
+ if ti.provides_identification
859
+ primary_ti ||= ti
860
+ else
861
+ other_ti ||= ti
862
+ end
863
+ end
864
+ ti = primary_ti || other_ti
865
+ # Make supertype steps first:
866
+ (ti.supertype == supertype ? [] : subtype_steps(query, ti.supertype, supertype)) +
867
+ [subtype_step(query, ti)]
868
+ end
869
+
870
+ # If there's a query, build it and return a new RoleSequence containing the projected roles:
871
+ def query_over_role_sequence(role_sequence, join_over, joined_roles, end_points)
872
+ # Skip if there's no query here (sequence join nor end-point subset join)
873
+ role_refs = role_sequence.all_role_ref_in_order
874
+ if !join_over and !role_refs.detect{|rr| rr.role.object_type != end_points[rr.ordinal]}
875
+ # No sequence join nor end_point join here
876
+ return role_sequence
877
+ end
878
+
879
+ # A RoleSequence for the actual query end-points
880
+ replacement_rs = @constellation.RoleSequence(:new)
881
+
882
+ query = @constellation.Query(:new)
883
+ variable = nil
884
+ query_role = nil
885
+ role_refs.zip(joined_roles||[]).each_with_index do |(role_ref, joined_role), i|
886
+
887
+ # Each role_ref is to an object joined via joined_role to variable (or which will be the variable)
888
+
889
+ # Create a variable for the actual end-point (supertype of the constrained roles)
890
+ end_point = end_points[i]
891
+ unless end_point
892
+ raise "In #{constraint_type} #{name}, there is a faulty or non-translated query"
893
+ end
894
+ trace :query, "Variable #{query.all_variable.size} is for #{end_point.name}"
895
+ end_node = @constellation.Variable(query, query.all_variable.size, :object_type => end_point)
896
+
897
+ # We're going to rewrite the constraint to constrain the supertype roles, but assume they're the same:
898
+ role_node = end_node
899
+ end_role = role_ref.role
900
+
901
+ # Create subtyping steps at the end-point, if needed:
902
+ projecting_play = nil
903
+ constrained_play = nil
904
+ if (subtype = role_ref.role.object_type) != end_point
905
+ trace :query, "Making subtyping steps from #{subtype.name} to #{end_point.name}" do
906
+ # There may be more than one supertyping level. Make the steps:
907
+ subtyping_steps = subtype_steps(query, subtype, end_point)
908
+ step = subtyping_steps[0]
909
+ #constrained_play = subtyping_steps[-1].all_play.detect{|p| p.role.object_type == end_point}
910
+ subtyping_steps.detect{|s| constrained_play = s.all_play.detect{|p| p.role.object_type == end_point}}
911
+
912
+ # Replace the constrained role and node with the supertype ones:
913
+ end_node = query.all_variable.detect{|jn| jn.object_type == end_point }
914
+ #projecting_play = step.all_play.detect{|p| p.role.object_type == subtype}
915
+ subtyping_steps.detect{|s| projecting_play = s.all_play.detect{|p| p.role.object_type == subtype}}
916
+ end_role = step.fact_type.all_role.detect{|r| r.object_type == end_point }
917
+ role_node = query.all_variable.detect{|jn| jn.object_type == role_ref.role.object_type }
918
+ end
919
+ end
920
+
921
+ if end_role.object_type != end_node.object_type
922
+ raise "Internal error in #{constraint_type} #{name}: making illegal reference to variable, end role mismatch"
923
+ end
924
+
925
+ if join_over
926
+ if !variable # Create the Variable when processing the first role
927
+ trace :query, "Variable #{query.all_variable.size} is over #{join_over.name}"
928
+ variable = @constellation.Variable(query, query.all_variable.size, :object_type => join_over)
929
+ end
930
+ trace :query, "Making step from #{end_point.name} to #{join_over.name}" do
931
+ rs = @constellation.RoleSequence(:new)
932
+ # Detect the fact type over which we're stepping (may involve objectification)
933
+ raise "Internal error in #{constraint_type} #{name}: making illegal reference to variable, object type mismatch" if role_ref.role.object_type != role_node.object_type
934
+ step = @constellation.Step(:guid => :new, :fact_type => joined_role.fact_type)
935
+ @constellation.RoleRef(rs, 0, :role => role_ref.role)
936
+ role_play = @constellation.Play(:step => step, :variable => role_node, :role => role_ref.role, :is_input => true)
937
+ # Make the projected RoleRef:
938
+ rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => role_ref.role, :play => role_play)
939
+ raise "Internal error in #{constraint_type} #{name}: making illegal reference to variable, joined_role mismatch" if joined_role.object_type != variable.object_type
940
+ @constellation.RoleRef(rs, 1, :role => joined_role)
941
+ join_play = @constellation.Play(:step => step, :variable => variable, :role => joined_role)
942
+ trace :query, "New step #{step.describe}"
943
+ end
944
+ else
945
+ trace :query, "Need step for non-join_over role #{end_point.name} #{role_ref.describe} in #{role_ref.role.fact_type.default_reading}"
946
+ if (roles = role_ref.role.fact_type.all_role.to_a).size > 1
947
+ # Here we have an end join (step already created) but no sequence join
948
+ if variable
949
+ raise "Internal error in #{constraint_type} #{name}: making illegal step" if role_ref.role.object_type != role_node.object_type
950
+ step = @constellation.Step(:guid => :new, :fact_type => role_ref.role.fact_type)
951
+ join_play = @constellation.Play(:step => step, :variable => variable, :role => query_role, :is_input => true)
952
+ role_play = @constellation.Play(:step => step, :variable => role_node, :role => role_ref.role)
953
+ # Make the projected RoleRef:
954
+ rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => role_ref.role, :play => role_play)
955
+ roles -= [query_role, role_ref.role]
956
+ roles.each do |incidental_role|
957
+ jn = @constellation.Variable(query, query.all_variable.size, :object_type => incidental_role.object_type)
958
+ play = @constellation.Play(:step => step, :variable => jn, :role => incidental_role, :step => step)
959
+ end
960
+ else
961
+
962
+ if role_sequence.all_role_ref.size > 1
963
+ variable = role_node
964
+ query_role = role_ref.role
965
+
966
+ # Make the projected RoleRef:
967
+ rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => constrained_play.role, :play => constrained_play)
968
+ else
969
+ # We enter this fact type (requiring that a role be played) but don't exit it.
970
+ # I think this can only happen where we have subtyping steps, above.
971
+
972
+ # There's no query in this role sequence, so we'd drop off the bottom without doing the right things. Why?
973
+ # Without this case, Supervision.orm omits "that runs Company" from the exclusion constraint, and I'm not sure why.
974
+ # I think the "then" code causes it to drop out the bottom without making the step (which is otherwise made in every case, see CompanyDirectorEmployee for example)
975
+ step = @constellation.Step(:guid => :new, :fact_type => role_ref.role.fact_type)
976
+
977
+ # p constrained_play.role.object_type.name
978
+ # p projecting_play.role.object_type.name
979
+ # debugger
980
+
981
+ role_play = @constellation.Play(:step => step, :variable => role_node, :role => role_ref.role, :is_input => true)
982
+
983
+ # Make the projected RoleRef:
984
+ rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => constrained_play.role, :play => constrained_play)
985
+
986
+ # role_ref.role.fact_type.all_role.each do |role|
987
+ # next if role == role_play.role
988
+ # next if role_sequence.all_role_ref.detect{|rr| rr.role == role}
989
+ # jn = @constellation.Variable(query, query.all_variable.size, :object_type => role.object_type)
990
+ # play = @constellation.Play(:step => step, :variable => jn, :role => role)
991
+ # if role == role_ref.role
992
+ # # Make the projected RoleRef:
993
+ # rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => role, :play => play)
994
+ # end
995
+ # end
996
+
997
+ end
998
+ end
999
+ else
1000
+ # Unary fact type, make a Step from and to the constrained_play
1001
+ step = @constellation.Step(:guid => :new, :fact_type => role_ref.role.fact_type)
1002
+ play = @constellation.Play(:step => step, :variable => constrained_play.variable, :role => role_ref.role, :is_input => true)
1003
+ # Make the projected RoleRef:
1004
+ rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => role_ref.role, :play => play)
1005
+ end
1006
+ end
1007
+ end
1008
+ raise "hell" if replacement_rs.all_role_ref.size != role_sequence.all_role_ref.size
1009
+
1010
+ # Thoroughly check that this is a valid query
1011
+ query.validate
1012
+ trace :query, "Query has projected nodes #{replacement_rs.describe}"
1013
+ replacement_rs
1014
+ end
1015
+
1016
+ # Equality and subset join constraints involve two or more role sequences,
1017
+ # and the respective roles from each sequence must be compatible,
1018
+ # Compatibility might involve subtyping steps but not objectification steps
1019
+ # to the respective end-point (constrained object type).
1020
+ # Also, all roles in each sequence constitute a join over a single
1021
+ # object type, which might involve subtyping or objectification steps.
1022
+ #
1023
+ def make_queries(constraint_type, name, role_sequences)
1024
+ begin
1025
+ # Get the object types constrained for each position in the role sequences.
1026
+ # Supertyping steps may be needed to reach them.
1027
+ end_points = [] # An array of the common supertype for matching role_refs across the sequences
1028
+ end_step_needed = [] # An array of booleans indicating whether any role_sequence requires subtyping steps
1029
+ role_sequences[0].all_role_ref.size.times do |i|
1030
+ role_refs = role_sequences.map{|rs| rs.all_role_ref.detect{|rr| rr.ordinal == i}}
1031
+ if (fact_types = role_refs.map{|rr| rr.role.fact_type}).uniq.size == 1
1032
+ # $stderr.puts(role_sequences.map{|rs| rs.all_role_ref.map{|rr| rr.role.fact_type.describe(rr.role)}}.inspect)
1033
+ raise "In #{constraint_type} #{name} role sequence #{i}, there is a faulty join involving just 1 fact type: '#{fact_types[0].default_reading}'"
1034
+ end
1035
+ if (players = role_refs.map{|rr| rr.role.object_type}).uniq.size == 1
1036
+ # All roles in this set are played by the same object type
1037
+ end_point = players[0]
1038
+ end_step_needed[i] = false
1039
+ else
1040
+ # Can the players be joined using subtyping steps?
1041
+ common_supertypes = players[1..-1].
1042
+ inject(players[0].supertypes_transitive) do |remaining, player|
1043
+ remaining & player.supertypes_transitive
1044
+ end
1045
+ end_point = common_supertypes[0]
1046
+
1047
+ raise "constrained roles of #{constraint_type} #{name} are incompatible (#{players.map(&:name)*', '})" if common_supertypes.size == 0
1048
+ end_step_needed[i] = true
1049
+ end
1050
+ end_points[i] = end_point
1051
+ end
1052
+
1053
+ # For each role_sequence, find the object type over which the join is implied (nil if no join)
1054
+ sequence_join_over = []
1055
+ if role_sequences[0].all_role_ref.size > 1 # There are queries within each sequence.
1056
+ sequence_join_over = []
1057
+ sequence_joined_roles = []
1058
+ role_sequences.map do |rs|
1059
+ join_over, joined_roles = *ActiveFacts::Metamodel.plays_over(rs.all_role_ref.map{|rr| rr.role})
1060
+ sequence_join_over << join_over
1061
+ sequence_joined_roles << joined_roles
1062
+ end
1063
+ end
1064
+
1065
+ # If there are no queries, we can drop out here.
1066
+ if sequence_join_over.compact.empty? && !end_step_needed.detect{|e| e}
1067
+ return true
1068
+ end
1069
+
1070
+ trace :query, "#{constraint_type} join constraint #{name} over #{role_sequences.map{|rs|rs.describe}*', '}"
1071
+
1072
+ query = nil
1073
+ trace :query, "#{constraint_type} join constraint #{name} constrains #{
1074
+ end_points.zip(end_step_needed).map{|(p,j)| (p ? p.name : 'NULL')+(j ? ' & subtypes':'')}*', '
1075
+ }#{
1076
+ if role_sequences[0].all_role_ref.size > 1
1077
+ ", joined over #{
1078
+ sequence_join_over.zip(sequence_joined_roles).map{|o, roles|
1079
+ (o ? o.name : '(none)') +
1080
+ (roles ? " to (#{roles.map{|role| role ? role.fact_type.default_reading : 'null'}*','})" : '')
1081
+ }*', '}"
1082
+ else
1083
+ ''
1084
+ end
1085
+ }" do
1086
+
1087
+ # There may be one query per role sequence:
1088
+ role_sequences.zip(sequence_join_over||[], sequence_joined_roles||[]).map do |role_sequence, join_over, joined_roles|
1089
+ position = role_sequences.index(role_sequence)
1090
+ replacement_rs = query_over_role_sequence(role_sequence, join_over, joined_roles, end_points)
1091
+ if role_sequence != replacement_rs
1092
+ role_sequences[position] = replacement_rs
1093
+ end
1094
+ end
1095
+
1096
+ return true
1097
+ end
1098
+ rescue => e
1099
+ debugger if trace :debug
1100
+ $stderr.puts "// #{e.to_s}: #{e.backtrace[0]}"
1101
+ return false
1102
+ end
1103
+
1104
+ end
1105
+
1106
+ def read_exclusion_constraints
1107
+ x_exclusion_constraints = @x_model.xpath("orm:Constraints/orm:ExclusionConstraint")
1108
+ trace :orm, "Reading #{x_exclusion_constraints.size} exclusion constraints" do
1109
+ x_exclusion_constraints.each{|x|
1110
+ id = x['id']
1111
+ name = x["Name"] || ''
1112
+ name = nil if name.size == 0
1113
+ x_mandatory = (m = x.xpath("orm:ExclusiveOrMandatoryConstraint")[0]) &&
1114
+ @x_by_id[mc_id = m['ref']]
1115
+ role_sequences =
1116
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
1117
+ x_role_refs = x_rs.xpath("orm:Role")
1118
+ map_roles(
1119
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
1120
+ "exclusion constraint #{name}"
1121
+ )
1122
+ }
1123
+ if x_mandatory
1124
+ # Remove absorbed mandatory constraints, leaving residual ones.
1125
+ mc_rs = @mandatory_constraint_rs_by_id[mc_id]
1126
+ @mandatory_constraint_rs_by_id.delete(mc_id)
1127
+ @mandatory_constraints_by_rs.delete(mc_rs)
1128
+ end
1129
+
1130
+ if role_sequences.compact.size != role_sequences.size # Role sequence missing; includes a derived fact type role
1131
+ trace :orm, "skipped exclusion constraint #{id}, missing role sequence"
1132
+ next
1133
+ end
1134
+
1135
+ unless make_queries('exclusion', name+(x_mandatory ? '/'+x_mandatory['Name'] : ''), role_sequences)
1136
+ trace :orm, "skipped exclusion constraint #{id}, can't make_queries"
1137
+ next
1138
+ end
1139
+
1140
+ ec = @constellation.SetExclusionConstraint(id_of(x))
1141
+ ec.vocabulary = @vocabulary
1142
+ ec.name = name
1143
+ # ec.enforcement =
1144
+ role_sequences.each_with_index do |rs, i|
1145
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
1146
+ end
1147
+ ec.is_mandatory = true if x_mandatory
1148
+ @by_id[id] = ec
1149
+ @by_id[mc_id] = ec if mc_id
1150
+ }
1151
+ end
1152
+ end
1153
+
1154
+ def read_equality_constraints
1155
+ x_equality_constraints = @x_model.xpath("orm:Constraints/orm:EqualityConstraint")
1156
+ trace :orm, "Reading equality constraints" do
1157
+ x_equality_constraints.each{|x|
1158
+ id = x['id']
1159
+ name = x["Name"] || ''
1160
+ name = nil if name.size == 0
1161
+ role_sequences =
1162
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
1163
+ x_role_refs = x_rs.xpath("orm:Role")
1164
+ map_roles(
1165
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
1166
+ "equality constraint #{name}"
1167
+ )
1168
+ }
1169
+
1170
+ # Role sequence missing; includes a derived fact type role
1171
+ next if role_sequences.compact.size != role_sequences.size
1172
+
1173
+ next unless make_queries('equality', name, role_sequences)
1174
+
1175
+ ec = @constellation.SetEqualityConstraint(id_of(x))
1176
+ ec.vocabulary = @vocabulary
1177
+ ec.name = name
1178
+ # ec.enforcement =
1179
+ role_sequences.each_with_index do |rs, i|
1180
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
1181
+ end
1182
+ @by_id[id] = ec
1183
+ }
1184
+ end
1185
+ end
1186
+
1187
+ def read_subset_constraints
1188
+ x_subset_constraints = @x_model.xpath("orm:Constraints/orm:SubsetConstraint")
1189
+ trace :orm, "Reading subset constraints" do
1190
+ x_subset_constraints.each{|x|
1191
+ id = x['id']
1192
+ name = x["Name"] || ''
1193
+ name = nil if name.size == 0
1194
+ role_sequences =
1195
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
1196
+ x_role_refs = x_rs.xpath("orm:Role")
1197
+ map_roles(
1198
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
1199
+ "equality constraint #{name}"
1200
+ )
1201
+ }
1202
+ next if role_sequences.compact.size != role_sequences.size # Role sequence missing; includes a derived fact type role
1203
+ next unless make_queries('subset', name, role_sequences)
1204
+
1205
+ ec = @constellation.SubsetConstraint(id_of(x))
1206
+ ec.vocabulary = @vocabulary
1207
+ ec.name = name
1208
+ # ec.enforcement =
1209
+ ec.subset_role_sequence = role_sequences[0]
1210
+ ec.superset_role_sequence = role_sequences[1]
1211
+ @by_id[id] = ec
1212
+ }
1213
+ end
1214
+ end
1215
+
1216
+ def read_ring_constraints
1217
+ x_ring_constraints = @x_model.xpath("orm:Constraints/orm:RingConstraint")
1218
+ trace :orm, "Reading ring constraints" do
1219
+ x_ring_constraints.each{|x|
1220
+ id = x['id']
1221
+ name = x["Name"] || ''
1222
+ name = nil if name.size == 0
1223
+ ring_type = x["Type"]
1224
+
1225
+ from, to = *x.xpath("orm:RoleSequence/orm:Role").
1226
+ map do |xr|
1227
+ @by_id[xr['ref']]
1228
+ end
1229
+ next unless from && to # Roles missing; covers a derived fact type
1230
+ if from.object_type != to.object_type
1231
+ join_over, = *ActiveFacts::Metamodel.plays_over([from, to], :counterpart)
1232
+ raise "Ring constraint has incompatible players #{from.object_type.name}, #{to.object_type.name}" if !join_over
1233
+ trace :query, "join ring constraint over #{join_over.name}"
1234
+ end
1235
+ rc = @constellation.RingConstraint(id_of(x))
1236
+ rc.vocabulary = @vocabulary
1237
+ rc.name = name
1238
+ # rc.enforcement =
1239
+ rc.role = from
1240
+ rc.other_role = to
1241
+ rc.ring_type = ring_type.gsub(/PurelyReflexive/,'Reflexive')
1242
+ @by_id[id] = rc
1243
+ }
1244
+ end
1245
+ end
1246
+
1247
+ def read_frequency_constraints
1248
+ x_frequency_constraints = @x_model.xpath("orm:Constraints/orm:FrequencyConstraint")
1249
+ trace :orm, "Reading frequency constraints" do
1250
+ x_frequency_constraints.each do |x_frequency_constraint|
1251
+ id = x_frequency_constraint['id']
1252
+ min_frequency = x_frequency_constraint["MinFrequency"].to_i
1253
+ min_frequency = nil if min_frequency == 0
1254
+ max_frequency = x_frequency_constraint["MaxFrequency"].to_i
1255
+ max_frequency = nil if max_frequency == 0
1256
+ x_roles = x_frequency_constraint.xpath("orm:RoleSequence/orm:Role")
1257
+ role = @by_id[x_roles[0]["ref"]]
1258
+ role_sequence = @constellation.RoleSequence(:new)
1259
+ role_ref = @constellation.RoleRef(role_sequence, 0, :role => role)
1260
+ next unless role # Role missing; belongs to a derived fact type
1261
+ trace :orm, "FrequencyConstraint(min #{min_frequency.inspect} max #{max_frequency.inspect} over #{role.fact_type.describe(role)} #{id} role ref = #{x_roles[0]["ref"]}"
1262
+ @by_id[id] = @constellation.PresenceConstraint(
1263
+ id_of(x_frequency_constraint),
1264
+ :vocabulary => @vocabulary,
1265
+ :name => name = x_frequency_constraint["Name"] || '',
1266
+ :role_sequence => role_sequence,
1267
+ :is_mandatory => false,
1268
+ :min_frequency => min_frequency,
1269
+ :max_frequency => max_frequency,
1270
+ :is_preferred_identifier => false
1271
+ )
1272
+ end
1273
+ end
1274
+ end
1275
+
1276
+ def read_instances
1277
+ trace :orm, "Reading sample data" do
1278
+ population = @constellation.Population(@vocabulary, "sample", :concept => :new)
1279
+
1280
+ # Value instances first, then entities then facts:
1281
+
1282
+ x_values = @x_model.xpath("orm:Objects/orm:ValueType/orm:Instances/orm:ValueTypeInstance/orm:Value")
1283
+ #pp x_values.map{|v| [ v.parent['id'], v.text ] }
1284
+ trace :orm, "Reading sample values" do
1285
+ x_values.each{|v|
1286
+ id = v.parent['id']
1287
+ # Get details of the ValueType:
1288
+ xvt = v.parent.parent.parent
1289
+ vt_id = xvt['id']
1290
+ vtname = xvt['Name'] || ''
1291
+ #vtname.gsub!(/\s/,'')
1292
+ vtname = nil if vtname.size == 0
1293
+ vt = @by_id[vt_id]
1294
+ throw "ValueType #{vtname} not found" unless vt
1295
+
1296
+ i = @constellation.Instance(id_of(v.parent), :population => population, :object_type => vt, :value => [v.text, is_literal_string(v.text), nil])
1297
+ @by_id[id] = i
1298
+ # show_xmlobj(v)
1299
+ }
1300
+ end
1301
+
1302
+ # Use the "id" attribute of EntityTypeInstance
1303
+ x_entities = @x_model.xpath("orm:Objects/orm:EntityType/orm:Instances/orm:EntityTypeInstance")
1304
+ #pp x_entities
1305
+ # x_entities.each{|v| show_xmlobj(v) }
1306
+ last_et_id = nil
1307
+ last_et = nil
1308
+ et = nil
1309
+ trace :orm, "Reading sample entities" do
1310
+ x_entities.each{|v|
1311
+ id = v['id']
1312
+
1313
+ # Get details of the EntityType:
1314
+ xet = v.parent.parent
1315
+ et_id = xet['id']
1316
+ if (et_id != last_et_id)
1317
+ etname = xet['Name'] || ''
1318
+ #etname.gsub!(/\s/,'')
1319
+ etname = nil if etname.size == 0
1320
+ last_et = et = @by_id[et_id]
1321
+ last_et_id = et_id
1322
+ throw "EntityType #{etname} not found" unless et
1323
+ end
1324
+
1325
+ instance = @constellation.Instance(id_of(v), :population => population, :object_type => et, :value => nil)
1326
+ @by_id[id] = instance
1327
+ trace :orm, "Made new EntityType #{etname}"
1328
+ }
1329
+ end
1330
+
1331
+ # The EntityType instances have implicit facts for the PI facts.
1332
+ # These are in the ORM file, but instead of using those,
1333
+ # We create implicit PI facts after all the instances.
1334
+ entity_count = 0
1335
+ pi_fact_count = 0
1336
+ trace :orm, "Creating identifying facts for entities" do
1337
+ x_entities.each do |v|
1338
+ id = v['id']
1339
+ instance = @by_id[id]
1340
+ et = @by_id[v.parent.parent['id']]
1341
+ next unless (preferred_id = et.preferred_identifier)
1342
+
1343
+ trace :orm, "Create identifying facts using #{preferred_id}"
1344
+
1345
+ # Collate the referenced objects by role:
1346
+ role_instances = v.elements[0].elements.inject({}){|h, v|
1347
+ etri = @x_by_id[v['ref']]
1348
+ x_role_id = etri.parent.parent['id']
1349
+ role = @by_id[x_role_id]
1350
+ object = @by_id[object_id = etri['ref']]
1351
+ h[role] = object
1352
+ h
1353
+ }
1354
+
1355
+ # Create an instance of each required fact type, for compound identification:
1356
+ identifying_fact_types =
1357
+ preferred_id.role_sequence.all_role_ref.map { |rr| rr.role.fact_type }.uniq
1358
+ identifying_fact_types.
1359
+ each do |ft|
1360
+ trace :orm, "For FactType #{ft}" do
1361
+ fact = @constellation.Fact(:new, :population => population, :fact_type => ft)
1362
+ fact_roles = ft.all_role.map do |role|
1363
+ if role.object_type == et
1364
+ object = instance
1365
+ else
1366
+ object = role_instances[role]
1367
+ trace :orm, "instance for role #{role} is #{object}"
1368
+ end
1369
+ @constellation.RoleValue(:instance => object, :population => population, :fact => fact, :role => role)
1370
+ end
1371
+ end
1372
+ pi_fact_count += 1
1373
+ end
1374
+
1375
+ entity_count += 1
1376
+ end
1377
+ end
1378
+ trace :orm, "Created #{pi_fact_count} facts to identify #{entity_count} entities"
1379
+
1380
+ # Use the "ref" attribute of FactTypeRoleInstance:
1381
+ x_fact_roles = @x_model.xpath("orm:Facts/orm:Fact/orm:Instances/orm:FactTypeInstance/orm:RoleInstances/orm:FactTypeRoleInstance")
1382
+
1383
+ # REVISIT: This presumably duplicates the identifying fact instances for the above entities. Hmmm.
1384
+ last_id = nil
1385
+ fact = nil
1386
+ fact_roles = []
1387
+ trace :orm, "Reading sample facts" do
1388
+ x_fact_roles.each do |v|
1389
+ fact_type_id = v.parent.parent.parent.parent['id']
1390
+ id = v.parent.parent['id']
1391
+ fact_type = @by_id[fact_type_id]
1392
+ throw "Fact type #{fact_type_id} not found" unless fact_type
1393
+
1394
+ # Create initial and subsequent Fact objects:
1395
+ fact = @constellation.Fact(:new, :population => population, :fact_type => fact_type) unless fact && last_id == id
1396
+ last_id = id
1397
+
1398
+ # REVISIT: This doesn't handle instances of objectified fact types (where a RoleValue.instance objectifies Fact)
1399
+
1400
+ x_role_instance = @x_by_id[v['ref']]
1401
+ x_role_id = x_role_instance.parent.parent['id']
1402
+ role = @by_id[x_role_id]
1403
+ throw "Role not found for instance #{x_role_id}" unless role
1404
+ instance_id = x_role_instance['ref']
1405
+ instance = @by_id[instance_id]
1406
+ throw "Instance not found for FactRole #{instance_id}" unless instance
1407
+ @constellation.RoleValue(:instance => instance, :population => population, :fact => fact, :role => role)
1408
+ end
1409
+ end
1410
+
1411
+ end
1412
+ end
1413
+
1414
+ def read_diagrams
1415
+ x_diagrams = @document.root.xpath("ormDiagram:ORMDiagram")
1416
+ trace :orm, "Reading diagrams" do
1417
+ x_diagrams.each do |x|
1418
+ name = (x["Name"] || '').strip
1419
+ diagram = @constellation.ORMDiagram(@vocabulary, name)
1420
+ trace :diagram, "Starting to read diagram #{name}"
1421
+ shapes = x.xpath("ormDiagram:Shapes/*")
1422
+ trace :orm, "Reading shapes" do
1423
+ shapes.map do |x_shape|
1424
+ x_subject = x_shape.xpath("ormDiagram:Subject")[0]
1425
+ subject = @by_id[x_subject["ref"]]
1426
+ is_expanded = v = x_shape['IsExpanded'] and v == 'true'
1427
+ bounds = x_shape['AbsoluteBounds']
1428
+ case shape_type = x_shape.name
1429
+ when 'FactTypeShape'
1430
+ if subject
1431
+ read_fact_type_shape diagram, x_shape, is_expanded, bounds, subject
1432
+ # else REVISIT: probably a derived fact type
1433
+ end
1434
+ when 'ExternalConstraintShape', 'FrequencyConstraintShape'
1435
+ # REVISIT: The offset might depend on the constraint type. This is right for subset and other round ones.
1436
+ location = convert_location(bounds, Gravity::C)
1437
+ shape = @constellation.ConstraintShape(
1438
+ :guid => id_of(x_shape), :orm_diagram => diagram, :location => location, :is_expanded => is_expanded,
1439
+ :constraint => subject
1440
+ )
1441
+ when 'RingConstraintShape'
1442
+ # REVISIT: The offset might depend on the ring constraint type. This is right for basic round ones.
1443
+ location = convert_location(bounds, Gravity::C)
1444
+ shape = @constellation.RingConstraintShape(
1445
+ :guid => id_of(x_shape), :orm_diagram => diagram, :location => location, :is_expanded => is_expanded,
1446
+ :constraint => subject
1447
+ )
1448
+ shape.fact_type_shape = subject.role.fact_type.all_fact_type_shape.to_a[0]
1449
+ when 'ModelNoteShape'
1450
+ # REVISIT: Add model notes
1451
+ when 'ObjectTypeShape'
1452
+ location = convert_location(bounds, Gravity::C)
1453
+ # $stderr.puts "#{subject.name}: bounds=#{bounds} -> location = (#{location.x}, #{location.y})"
1454
+ shape = @constellation.ObjectTypeShape(
1455
+ :guid => id_of(x_shape), :orm_diagram => diagram, :location => location, :is_expanded => is_expanded,
1456
+ :object_type => subject,
1457
+ :location => location
1458
+ )
1459
+ else
1460
+ raise "Unknown shape #{x_shape.name}"
1461
+ end
1462
+ end
1463
+ end
1464
+ end
1465
+ end
1466
+ end
1467
+
1468
+ def read_fact_type_shape diagram, x_shape, is_expanded, bounds, fact_type
1469
+ display_role_names_setting = v = x_shape["DisplayRoleNames"] and
1470
+ case v
1471
+ when 'Off'; 'false'
1472
+ when 'On'; 'true'
1473
+ else nil
1474
+ end
1475
+ rotation_setting = v = x_shape['DisplayOrientation'] and
1476
+ case v
1477
+ when 'VerticalRotatedLeft'; 'left'
1478
+ when 'VerticalRotatedRight'; 'right'
1479
+ else nil
1480
+ end
1481
+
1482
+ # Location of a fact type is the centre of the row of role boxes
1483
+ offs_x = 11
1484
+ offs_y = -12
1485
+ if fact_type.entity_type
1486
+ offs_x -= 12
1487
+ offs_y -= 9 if !fact_type.entity_type.concept.implication_rule # .implication_rule_name == 'objectification'
1488
+ end
1489
+
1490
+ location = convert_location(bounds, Gravity::S, offs_x, offs_y)
1491
+
1492
+ # $stderr.puts "#{fact_type.describe}: bounds=#{bounds} -> location = (#{location.x}, #{location.y})"
1493
+
1494
+ trace :orm, "REVISIT: Can't place rotated fact type correctly on diagram yet" if rotation_setting
1495
+
1496
+ trace :orm, "fact type at #{location.x},#{location.y} has display_role_names_setting=#{display_role_names_setting.inspect}, rotation_setting=#{rotation_setting.inspect}"
1497
+ shape = @constellation.FactTypeShape(
1498
+ :guid => id_of(x_shape),
1499
+ :orm_diagram => diagram,
1500
+ :location => location,
1501
+ :is_expanded => is_expanded,
1502
+ :display_role_names_setting => display_role_names_setting,
1503
+ :rotation_setting => rotation_setting,
1504
+ :fact_type => fact_type
1505
+ )
1506
+ # Create RoleDisplay objects if necessary
1507
+ x_role_display = x_shape.xpath("ormDiagram:RoleDisplayOrder/ormDiagram:Role")
1508
+ # print "Fact type '#{fact_type.preferred_reading.expand}' (#{fact_type.all_role.map{|r|r.object_type.name}*' '})"
1509
+ if x_role_display.size > 0
1510
+ trace :orm, " has roleDisplay (#{x_role_display.map{|rd| @by_id[rd['ref']].object_type.name}*','})'"
1511
+ x_role_display.each_with_index do |rd, ordinal|
1512
+ role_display = @constellation.RoleDisplay(shape, ordinal, :role => @by_id[rd['ref']])
1513
+ end
1514
+ else
1515
+ # Decide whether to create all RoleDisplay objects for this fact type, which is in role order
1516
+ # Omitting this here might lead to incomplete RoleDisplay sequences,
1517
+ # because each RoleNameShape or ValueConstraintShape creates just one.
1518
+ trace :orm, " has no roleDisplay"
1519
+ end
1520
+
1521
+ relative_shapes = x_shape.xpath('ormDiagram:RelativeShapes/*')
1522
+ relative_shapes.each do |xr_shape|
1523
+ location = convert_location(xr_shape['AbsoluteBounds'])
1524
+ case xr_shape.name
1525
+ when 'ObjectifiedFactTypeNameShape'
1526
+ @constellation.ObjectifiedFactTypeNameShape(:guid => id_of(xr_shape), :fact_type_shape => shape, :orm_diagram => diagram, :location => location, :is_expanded => false)
1527
+ when 'ReadingShape'
1528
+ begin
1529
+ @constellation.ReadingShape(:guid => id_of(xr_shape), :fact_type_shape => shape, :orm_diagram => diagram, :location => location, :is_expanded => false, :reading => fact_type.preferred_reading)
1530
+ rescue =>e
1531
+ debugger
1532
+ @constellation.ReadingShape(:guid => id_of(xr_shape), :fact_type_shape => shape, :orm_diagram => diagram, :location => location, :is_expanded => false, :reading => fact_type.preferred_reading)
1533
+ end
1534
+ when 'RoleNameShape'
1535
+ role = @by_id[xr_shape.xpath("ormDiagram:Subject")[0]['ref']]
1536
+ role_display = role_display_for_role(shape, x_role_display, role)
1537
+ trace :orm, "Fact type '#{fact_type.preferred_reading.expand}' has #{xr_shape.name}"
1538
+ @constellation.RoleNameShape(
1539
+ :guid => id_of(xr_shape), :orm_diagram => diagram, :location => location, :is_expanded => false,
1540
+ :role_display => role_display
1541
+ )
1542
+ when 'ValueConstraintShape'
1543
+ vc_subject_id = xr_shape.xpath("ormDiagram:Subject")[0]['ref']
1544
+ constraint = @by_id[vc_subject_id]
1545
+ trace :orm, "Fact type '#{fact_type.preferred_reading.expand}' has #{xr_shape.name} for #{constraint.inspect}"
1546
+
1547
+ role_display = role_display_for_role(shape, x_role_display, constraint.role)
1548
+ trace :orm, "ValueConstraintShape is on #{role_display.ordinal}'th role (by #{x_role_display.size > 0 ? 'role_display' : 'fact roles'})"
1549
+ @constellation.ValueConstraintShape(
1550
+ :guid => id_of(xr_shape), :orm_diagram => diagram, :location => location, :is_expanded => false,
1551
+ :constraint => constraint,
1552
+ :object_type_shape => nil, # This constraint is relative to a Fact Type, so must be on a role
1553
+ :role_display => role_display
1554
+ )
1555
+ else raise "Unknown relative shape #{xr_shape.name}"
1556
+ end
1557
+ end
1558
+ end
1559
+
1560
+ # Find or create the RoleDisplay for this role in this fact_type_shape, given (possibly empty) x_role_display nodes:
1561
+ def role_display_for_role(fact_type_shape, x_role_display, role)
1562
+ if x_role_display.size == 0
1563
+ # There's no x_role_display, which means the roles are in displayed
1564
+ # the same order as in the fact type. However, we need a RoleDisplay
1565
+ # to attach a ReadingShape or ValueConstraintShape, so make them all.
1566
+ fact_type_shape.fact_type.all_role.each{|r| @constellation.RoleDisplay(fact_type_shape, r.ordinal, :role => r) }
1567
+ role_ordinal = fact_type_shape.fact_type.all_role_in_order.index(role)
1568
+ else
1569
+ role_ordinal = x_role_display.map{|rd| @by_id[rd['ref']]}.index(role)
1570
+ end
1571
+ role_display = @constellation.RoleDisplay(fact_type_shape, role_ordinal, :role => role)
1572
+ end
1573
+
1574
+ DIAGRAM_SCALE = 96*1.5
1575
+ def convert_location(bounds, gravity = Gravity::C, xoffs = 0, yoffs = 0)
1576
+ return nil unless bounds
1577
+ # Bounds is top, left, width, height in inches
1578
+ bf = bounds.split(/, /).map{|b|b.to_f}
1579
+ sizefrax = [
1580
+ [0, 0], [1, 0], [2, 0],
1581
+ [0, 1], [1, 1], [2, 2],
1582
+ [0, 2], [1, 2], [2, 2],
1583
+ ]
1584
+
1585
+ x = (DIAGRAM_SCALE * (bf[0]+bf[2]*sizefrax[gravity][0]/2)).round + xoffs
1586
+ y = (DIAGRAM_SCALE * (bf[1]+bf[3]*sizefrax[gravity][1]/2)).round + yoffs
1587
+ @constellation.Location(x, y)
1588
+ end
1589
+
1590
+ # Detect numeric data and denote it as a string:
1591
+ def is_literal_string(value)
1592
+ value =~ /[^ \d.]/
1593
+ end
1594
+
1595
+ def read_rest
1596
+ puts "Reading Implied Facts (not yet)"
1597
+ =begin
1598
+ x_implied_facts = @x_model.xpath("orm:Facts/orm:ImpliedFact")
1599
+ pp x_implied_facts
1600
+ =end
1601
+ puts "Reading Data Types (not yet)"
1602
+ =begin
1603
+ x_datatypes = @x_model.xpath("orm:DataTypes/*")
1604
+ pp x_datatypes
1605
+ =end
1606
+ puts "Reading Reference Mode Kinds (not yet)"
1607
+ =begin
1608
+ x_refmodekinds = @x_model.xpath("orm:ReferenceModeKinds/*")
1609
+ pp x_refmodekinds
1610
+ =end
1611
+ end
1612
+
1613
+ def show_xmlobj(x, indent = "")
1614
+ parentage = []
1615
+ p = x
1616
+ while (p)
1617
+ parentage.unshift(p)
1618
+ p = p.parent
1619
+ end
1620
+ #parentage = parentage.shift
1621
+ puts "#{indent}#{x.name} object has heritage {"
1622
+ parentage.each{|p|
1623
+ next if REXML::Document === p
1624
+ puts "#{indent}\t#{p.name}#{
1625
+ }#{(n = p['Name']) ? " Name='#{n}'" : ""
1626
+ }#{(id = p['id']) ? " #{id}" : ""
1627
+ }#{(ref = p['ref']) ? " -> #{ref}" : ""
1628
+ }#{/\S/ === ((text = p.text)) ? " "+text.inspect : ""
1629
+ }"
1630
+ show_xmlobj(@x_by_id[ref], "\t#{indent}") if ref
1631
+ }
1632
+ puts "#{indent}}"
1633
+ end
1634
+ end
1635
+ end
1636
+ end