activefacts-orm 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
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