activefacts 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +83 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +41 -0
  5. data/bin/afgen +46 -0
  6. data/bin/cql +52 -0
  7. data/examples/CQL/Address.cql +46 -0
  8. data/examples/CQL/Blog.cql +54 -0
  9. data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
  10. data/examples/CQL/Death.cql +16 -0
  11. data/examples/CQL/Genealogy.cql +95 -0
  12. data/examples/CQL/Marriage.cql +18 -0
  13. data/examples/CQL/Metamodel.cql +238 -0
  14. data/examples/CQL/MultiInheritance.cql +19 -0
  15. data/examples/CQL/OilSupply.cql +47 -0
  16. data/examples/CQL/Orienteering.cql +108 -0
  17. data/examples/CQL/PersonPlaysGame.cql +17 -0
  18. data/examples/CQL/SchoolActivities.cql +31 -0
  19. data/examples/CQL/SimplestUnary.cql +12 -0
  20. data/examples/CQL/SubtypePI.cql +32 -0
  21. data/examples/CQL/Warehousing.cql +99 -0
  22. data/examples/CQL/WindowInRoomInBldg.cql +22 -0
  23. data/lib/activefacts.rb +10 -0
  24. data/lib/activefacts/api.rb +25 -0
  25. data/lib/activefacts/api/concept.rb +384 -0
  26. data/lib/activefacts/api/constellation.rb +106 -0
  27. data/lib/activefacts/api/entity.rb +239 -0
  28. data/lib/activefacts/api/instance.rb +54 -0
  29. data/lib/activefacts/api/numeric.rb +158 -0
  30. data/lib/activefacts/api/role.rb +94 -0
  31. data/lib/activefacts/api/standard_types.rb +67 -0
  32. data/lib/activefacts/api/support.rb +59 -0
  33. data/lib/activefacts/api/value.rb +122 -0
  34. data/lib/activefacts/api/vocabulary.rb +120 -0
  35. data/lib/activefacts/cql.rb +31 -0
  36. data/lib/activefacts/cql/CQLParser.treetop +104 -0
  37. data/lib/activefacts/cql/Concepts.treetop +112 -0
  38. data/lib/activefacts/cql/DataTypes.treetop +66 -0
  39. data/lib/activefacts/cql/Expressions.treetop +113 -0
  40. data/lib/activefacts/cql/FactTypes.treetop +185 -0
  41. data/lib/activefacts/cql/Language/English.treetop +92 -0
  42. data/lib/activefacts/cql/LexicalRules.treetop +169 -0
  43. data/lib/activefacts/cql/Rakefile +6 -0
  44. data/lib/activefacts/cql/parser.rb +88 -0
  45. data/lib/activefacts/generate/absorption.rb +87 -0
  46. data/lib/activefacts/generate/cql.rb +441 -0
  47. data/lib/activefacts/generate/cql/html.rb +397 -0
  48. data/lib/activefacts/generate/null.rb +19 -0
  49. data/lib/activefacts/generate/ordered.rb +557 -0
  50. data/lib/activefacts/generate/ruby.rb +326 -0
  51. data/lib/activefacts/generate/sql/server.rb +164 -0
  52. data/lib/activefacts/generate/text.rb +21 -0
  53. data/lib/activefacts/input/cql.rb +1268 -0
  54. data/lib/activefacts/input/orm.rb +926 -0
  55. data/lib/activefacts/persistence.rb +1 -0
  56. data/lib/activefacts/persistence/composition.rb +653 -0
  57. data/lib/activefacts/support.rb +51 -0
  58. data/lib/activefacts/version.rb +3 -0
  59. data/lib/activefacts/vocabulary.rb +6 -0
  60. data/lib/activefacts/vocabulary/extensions.rb +343 -0
  61. data/lib/activefacts/vocabulary/metamodel.rb +303 -0
  62. data/script/txt2html +71 -0
  63. data/spec/absorption_spec.rb +95 -0
  64. data/spec/api/autocounter.rb +82 -0
  65. data/spec/api/constellation.rb +130 -0
  66. data/spec/api/entity_type.rb +101 -0
  67. data/spec/api/instance.rb +428 -0
  68. data/spec/api/roles.rb +122 -0
  69. data/spec/api/value_type.rb +112 -0
  70. data/spec/api_spec.rb +14 -0
  71. data/spec/cql_cql_spec.rb +58 -0
  72. data/spec/cql_parse_spec.rb +31 -0
  73. data/spec/cql_ruby_spec.rb +60 -0
  74. data/spec/cql_sql_spec.rb +54 -0
  75. data/spec/cql_symbol_tables_spec.rb +259 -0
  76. data/spec/cql_unit_spec.rb +336 -0
  77. data/spec/cqldump_spec.rb +169 -0
  78. data/spec/norma_cql_spec.rb +48 -0
  79. data/spec/norma_ruby_spec.rb +50 -0
  80. data/spec/norma_sql_spec.rb +45 -0
  81. data/spec/norma_tables_spec.rb +94 -0
  82. data/spec/spec.opts +1 -0
  83. data/spec/spec_helper.rb +10 -0
  84. metadata +173 -0
@@ -0,0 +1,926 @@
1
+ #
2
+ # Read a NORMA file into an ActiveFacts vocabulary
3
+ #
4
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
5
+ #
6
+ # This code uses variables prefixed with x_ when they refer to Rexml nodes.
7
+ # Every node having an id="..." is indexed in @x_by_id[] hash before processing.
8
+ # As we build ActiveFacts objects to match, we index those in @by_id[].
9
+ # Both these hashes may be looked up by any of the ref="..." values in the file.
10
+ #
11
+ require 'rexml/document'
12
+ require 'activefacts/vocabulary'
13
+
14
+ module ActiveFacts
15
+ module Input
16
+ # The ORM Input module is activated whenever afgen is called upon to process a file
17
+ # whose name ends in .orm (a file from NORMA). This parser uses Rexml so it's very slow.
18
+ # The file is parsed to a constellation and the vocabulary object defined in that file is returned.
19
+ class ORM
20
+ def self.readfile(filename)
21
+ File.open(filename) {|file|
22
+ self.read(file, filename)
23
+ }
24
+ end
25
+
26
+ def self.read(file, filename = "stdin")
27
+ ORM.new(file, filename).read
28
+ end
29
+
30
+ def initialize(file, filename = "stdin")
31
+ @file = file
32
+ @filename = filename
33
+ end
34
+
35
+ def read
36
+ begin
37
+ @document = REXML::Document.new(@file)
38
+ rescue => e
39
+ puts "Failed to parse XML in #{@filename}: #{e.inspect}"
40
+ end
41
+
42
+ # Find the Vocabulary and do some setup:
43
+ root = @document.elements[1]
44
+ if root.expanded_name == "ormRoot:ORM2"
45
+ x_models = root.elements.to_a("orm:ORMModel")
46
+ throw "No vocabulary found" unless x_models.size == 1
47
+ @x_model = x_models[0]
48
+ elsif root.name == "ORMModel"
49
+ @x_model = @document.elements[1]
50
+ else
51
+ pp root
52
+ throw "NORMA vocabulary not found in file"
53
+ end
54
+
55
+ read_vocabulary
56
+ @vocabulary
57
+ end
58
+
59
+ def read_vocabulary
60
+ @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
61
+ @vocabulary = @constellation.Vocabulary(@x_model.attributes['Name'])
62
+
63
+ # Find all elements having an "id" attribute and index them
64
+ x_identified = @x_model.elements.to_a("//*[@id]")
65
+ @x_by_id = x_identified.inject({}){|h, x|
66
+ id = x.attributes['id']
67
+ h[id] = x
68
+ h
69
+ }
70
+
71
+ # Everything we build will be indexed here:
72
+ @by_id = {}
73
+
74
+ read_entity_types
75
+ read_value_types
76
+ read_fact_types
77
+ read_nested_types
78
+ read_subtypes
79
+ read_roles
80
+ read_constraints
81
+ # REVISIT: Skip instance data for now:
82
+ #read_instances
83
+ end
84
+
85
+ def read_entity_types
86
+ # get and process all the entity types:
87
+ entity_types = []
88
+ x_entity_types = @x_model.elements.to_a("orm:Objects/orm:EntityType")
89
+ x_entity_types.each{|x|
90
+ id = x.attributes['id']
91
+ name = x.attributes['Name'] || ""
92
+ name.gsub!(/\s/,'')
93
+ name = nil if name.size == 0
94
+ # puts "EntityType #{name} is #{id}"
95
+ entity_types <<
96
+ @by_id[id] =
97
+ entity_type =
98
+ @constellation.EntityType(name, @vocabulary)
99
+ independent = x.attributes['IsIndependent']
100
+ entity_type.is_independent = true if independent && independent == 'true'
101
+ personal = x.attributes['IsPersonal']
102
+ entity_type.is_personal = true if personal && personal == 'true'
103
+ # x_pref = x.elements.to_a("orm:PreferredIdentifier")[0]
104
+ # if x_pref
105
+ # pi_id = x_pref.attributes['ref']
106
+ # @pref_id_for[pi_id] = x
107
+ # end
108
+ }
109
+ end
110
+
111
+ def read_value_types
112
+ # Now the value types:
113
+ value_types = []
114
+ x_value_types = @x_model.elements.to_a("orm:Objects/orm:ValueType")
115
+ #pp x_value_types
116
+ x_value_types.each{|x|
117
+ id = x.attributes['id']
118
+ name = x.attributes['Name'] || ""
119
+ name.gsub!(/\s/,'')
120
+ name = nil if name.size == 0
121
+
122
+ cdt = x.elements.to_a('orm:ConceptualDataType')[0]
123
+ scale = cdt.attributes['Scale']
124
+ scale = scale != "" && scale.to_i
125
+ length = cdt.attributes['Length']
126
+ length = length != "" && length.to_i
127
+ base_type = @x_by_id[cdt.attributes['ref']]
128
+ type_name = "#{base_type.name}"
129
+ type_name.sub!(/^orm:/,'')
130
+ type_name.sub!(/DataType\Z/,'')
131
+ type_name.sub!(/Numeric\Z/,'')
132
+ type_name.sub!(/Temporal\Z/,'')
133
+ length = 32 if type_name =~ /Integer\Z/ && length.to_i == 0 # Set default integer length
134
+
135
+ # REVISIT: Need to handle standard types better here:
136
+ data_type = type_name != name ? @constellation.ValueType(type_name, @vocabulary) : nil
137
+
138
+ # puts "ValueType #{name} is #{id}"
139
+ value_types <<
140
+ @by_id[id] =
141
+ vt = @constellation.ValueType(name, @vocabulary)
142
+ vt.supertype = data_type
143
+ vt.length = length if length
144
+ vt.scale = scale if scale
145
+ independent = x.attributes['IsIndependent']
146
+ vt.is_independent = true if independent && independent == 'true'
147
+ personal = x.attributes['IsPersonal']
148
+ vt.is_personal = true if personal && personal == 'true'
149
+
150
+ x_ranges = x.elements.to_a("orm:ValueRestriction/orm:ValueConstraint/orm:ValueRanges/orm:ValueRange")
151
+ next if x_ranges.size == 0
152
+ vt.value_restriction = @constellation.ValueRestriction(:new)
153
+ x_ranges.each{|x_range|
154
+ v_range = value_range(x_range)
155
+ ar = @constellation.AllowedRange(v_range, vt.value_restriction)
156
+ }
157
+ }
158
+ end
159
+
160
+ def value_range(x_range)
161
+ min = x_range.attributes['MinValue']
162
+ max = x_range.attributes['MaxValue']
163
+ q = "'"
164
+ min = min =~ /[^0-9\.]/ ? q+min+q : min.to_i
165
+ max = max =~ /[^0-9\.]/ ? q+max+q : max.to_i
166
+ # ValueRange takes a minimum and/or a maximum Bound, each takes value and whether inclusive
167
+ @constellation.ValueRange(
168
+ min ? [min.to_s, true] : nil,
169
+ max ? [max.to_s, true] : nil
170
+ )
171
+ end
172
+
173
+ def read_fact_types
174
+ # Handle the fact types:
175
+ facts = []
176
+ @x_facts = @x_model.elements.to_a("orm:Facts/orm:Fact")
177
+ @x_facts.each{|x|
178
+ id = x.attributes['id']
179
+ name = x.attributes['Name'] || x.attributes['_Name']
180
+ name = "<unnamed>" if !name
181
+ name.gsub!(/\s/,'')
182
+ name = "" if !name || name.size == 0
183
+ # Note that the new metamodel doesn't have a name for a facttype unless it's objectified
184
+
185
+ # puts "FactType #{name || id}"
186
+ facts << @by_id[id] = fact_type = @constellation.FactType(:new)
187
+ }
188
+ end
189
+
190
+ def read_subtypes
191
+ # Handle the subtype fact types:
192
+ facts = []
193
+ @x_subtypes = @x_model.elements.to_a("orm:Facts/orm:SubtypeFact")
194
+ @x_mappings = @document.elements.to_a("ormRoot:ORM2/oialtocdb:MappingCustomization/oialtocdb:AssimilationMappings/oialtocdb:AssimilationMapping/oialtocdb:FactType")
195
+
196
+ @x_subtypes.each{|x|
197
+ id = x.attributes['id']
198
+ name = x.attributes['Name'] || x.attributes['_Name'] || ''
199
+ name.gsub!(/\s/,'')
200
+ name = nil if name.size == 0
201
+ # puts "FactType #{name || id}"
202
+
203
+ mapping = @x_mappings.detect{ |m| m.attributes['ref'] == id }
204
+ mapping_choice = mapping ? mapping.parent.attributes['AbsorptionChoice'] : 'Absorbed'
205
+
206
+ x_subtype_role = x.elements['orm:FactRoles/orm:SubtypeMetaRole']
207
+ subtype_role_id = x_subtype_role.attributes['id']
208
+ subtype_id = x_subtype_role.elements['orm:RolePlayer'].attributes['ref']
209
+ subtype = @by_id[subtype_id]
210
+ subtype.is_independent = true if mapping_choice == 'Separate'
211
+ # REVISIT: Provide a way in the metamodel of handling Partition, (and mapping choices that vary for each supertype?)
212
+
213
+ x_supertype_role = x.elements['orm:FactRoles/orm:SupertypeMetaRole']
214
+ supertype_role_id = x_supertype_role.attributes['id']
215
+ supertype_id = x_supertype_role.elements['orm:RolePlayer'].attributes['ref']
216
+ supertype = @by_id[supertype_id]
217
+
218
+ throw "For Subtype fact #{name}, the supertype #{supertype_id} was not found" if !supertype
219
+ throw "For Subtype fact #{name}, the subtype #{subtype_id} was not found" if !subtype
220
+ # $stderr.puts "#{subtype.name} is a subtype of #{supertype.name}"
221
+
222
+ inheritance_fact = @constellation.TypeInheritance(subtype, supertype)
223
+ inheritance_fact.fact_type_id = :new
224
+ if x.attributes["IsPrimary"] == "true" or # Old way
225
+ x.attributes["PreferredIdentificationPath"] == "true" # Newer
226
+ # $stderr.puts "#{supertype.name} is primary supertype of #{subtype.name}"
227
+ inheritance_fact.provides_identification = true
228
+ end
229
+ facts << @by_id[id] = inheritance_fact
230
+
231
+ # Create the new Roles so we can find constraints on them:
232
+ subtype_role = @by_id[subtype_role_id] = @constellation.Role(inheritance_fact, 0, subtype)
233
+ supertype_role = @by_id[supertype_role_id] = @constellation.Role(inheritance_fact, 1, supertype)
234
+
235
+ # Create readings, so constraints can be verbalised for example:
236
+ rs = @constellation.RoleSequence(:new)
237
+ @constellation.RoleRef(rs, 0).role = subtype_role
238
+ @constellation.RoleRef(rs, 1).role = supertype_role
239
+
240
+ # reading = @constellation.Reading(inheritance_fact, 0)
241
+ # reading.reading_text = "{1} is {0}"
242
+ # reading.role_sequence = rs
243
+
244
+ reading = @constellation.Reading(inheritance_fact, 0)
245
+ reading.reading_text = "{0} is a subtype of {1}"
246
+ reading.role_sequence = rs
247
+
248
+ # The required uniqueness constraints are already present in the NORMA file, don't duplicate them
249
+ =begin
250
+ # Create uniqueness constraints over the subtyping fact type
251
+ p1rs = @constellation.RoleSequence(:new)
252
+ @constellation.RoleRef(p1rs, 0).role = subtype_role
253
+ pc1 = @constellation.PresenceConstraint(:new)
254
+ pc1.name = "#{subtype.name}MustHaveSupertype#{supertype.name}"
255
+ pc1.vocabulary = @vocabulary
256
+ pc1.role_sequence = p1rs
257
+ pc1.is_mandatory = true # A subtype instance must have a supertype instance
258
+ pc1.min_frequency = 1
259
+ pc1.max_frequency = 1
260
+ pc1.is_preferred_identifier = false
261
+
262
+ # The supertype role often identifies the subtype:
263
+ p2rs = @constellation.RoleSequence(:new)
264
+ @constellation.RoleRef(p2rs, 0).role = supertype_role
265
+ pc2 = @constellation.PresenceConstraint(:new)
266
+ pc2.name = "#{supertype.name}MayBeA#{subtype.name}"
267
+ pc2.vocabulary = @vocabulary
268
+ pc2.role_sequence = p2rs
269
+ pc2.is_mandatory = false
270
+ pc2.min_frequency = 0
271
+ pc2.max_frequency = 1
272
+ pc2.is_preferred_identifier = inheritance_fact.provides_identification
273
+ =end
274
+ }
275
+ end
276
+
277
+ def read_nested_types
278
+ # Process NestedTypes, but ignore ones having a NestedPredicate with IsImplied="true"
279
+ # We'll ignore the fact roles (and constraints) that implied objectifications have.
280
+ # This happens for all ternaries and higher order facts
281
+ nested_types = []
282
+ x_nested_types = @x_model.elements.to_a("orm:Objects/orm:ObjectifiedType")
283
+ x_nested_types.each{|x|
284
+ id = x.attributes['id']
285
+ name = x.attributes['Name'] || ""
286
+ name.gsub!(/\s/,'')
287
+ name = nil if name.size == 0
288
+
289
+ x_fact_type = x.elements.to_a('orm:NestedPredicate')[0]
290
+ is_implied = x_fact_type.attributes['IsImplied'] == "true"
291
+
292
+ fact_id = x_fact_type.attributes['ref']
293
+ fact_type = @by_id[fact_id]
294
+ throw "Nested fact #{fact_id} not found" if !fact_type
295
+
296
+ #if is_implied
297
+ # puts "Implied type #{name} (#{id}) nests #{fact_type ? fact_type.fact_type_id : "unknown"}"
298
+ # @by_id[id] = fact_type
299
+ #else
300
+ begin
301
+ #puts "NestedType #{name} is #{id}, nests #{fact_type.fact_type_id}"
302
+ nested_types <<
303
+ @by_id[id] =
304
+ nested_type = @constellation.EntityType(name, @vocabulary)
305
+ nested_type.fact_type = fact_type
306
+ end
307
+ }
308
+ end
309
+
310
+ def read_roles
311
+ @x_facts.each{|x|
312
+ id = x.attributes['id']
313
+ fact_type = @by_id[id]
314
+ fact_name = x.attributes['Name'] || x.attributes['_Name'] || ''
315
+ fact_name.gsub!(/\s/,'')
316
+ fact_name = nil if fact_name == ''
317
+
318
+ x_fact_roles = x.elements.to_a('orm:FactRoles/*')
319
+ x_reading_orders = x.elements.to_a('orm:ReadingOrders/*')
320
+
321
+ # Deal with FactRoles (Roles):
322
+ x_fact_roles.each{|x|
323
+ name = x.attributes['Name'] || ""
324
+ name.gsub!(/\s/,'')
325
+ name = nil if name.size == 0
326
+
327
+ # _IsMandatory = x.attributes['_IsMandatory']
328
+ # _Multiplicity = x.attributes['_Multiplicity]
329
+ id = x.attributes['id']
330
+ ref = x.elements[1].attributes['ref']
331
+
332
+ # Find the concept that plays the role:
333
+ concept = @by_id[ref]
334
+ throw "RolePlayer for #{name||ref} was not found" if !concept
335
+
336
+ # Skip implicit roles added by NORMA to make unaries into binaries.
337
+ # This would make constraints over the deleted roles impossible,
338
+ # so as a SPECIAL CASE we index the unary role by the id of the
339
+ # implicit role. That means care is needed when handling unary FTs.
340
+ if (ox = @x_by_id[ref]) && ox.attributes['IsImplicitBooleanValue']
341
+ x_other_role = x.parent.elements.to_a('orm:Role').reject{|x_role|
342
+ x_role == x
343
+ }[0]
344
+ other_role_id = x_other_role.attributes["id"]
345
+ other_role = @by_id[other_role_id]
346
+ # puts "Indexing unary FT role #{other_role_id} by implicit boolean role #{id}"
347
+ @by_id[id] = other_role
348
+
349
+ # The role name of the ignored role is the one that applies:
350
+ role_name = x.attributes['Name']
351
+ other_role.role_name = role_name if role_name && role_name != ''
352
+
353
+ concept.delete # Delete our object for the implicit boolean ValueType
354
+ @by_id.delete(ref) # and de-index it from our list
355
+ next
356
+ end
357
+
358
+ #puts "#{@vocabulary}, Name=#{x.attributes['Name']}, concept=#{concept}"
359
+ throw "Role is played by #{concept.class} not Concept" if !(@constellation.vocabulary.concept(:Concept) === concept)
360
+
361
+ name = x.attributes['Name'] || ''
362
+ name.gsub!(/\s/,'')
363
+ name = nil if name.size == 0
364
+ #puts "Creating role #{name} nr#{fact_type.all_role.size} of #{fact_type.fact_type_id} played by #{concept.name}"
365
+
366
+ role = @by_id[id] = @constellation.Role(fact_type, fact_type.all_role.size, concept)
367
+ role.role_name = name if name
368
+ # puts "Fact #{fact_name} (id #{fact_type.fact_type_id.object_id}) role #{x.attributes['Name']} is played by #{concept.name}, role is #{role.object_id}"
369
+
370
+ x_vr = x.elements.to_a("orm:ValueRestriction")
371
+ x_vr.each{|vr|
372
+ x_ranges = vr.elements.to_a("orm:RoleValueConstraint/orm:ValueRanges/orm:ValueRange")
373
+ next if x_ranges.size == 0
374
+ role.role_value_restriction = @constellation.ValueRestriction(:new)
375
+ x_ranges.each{|x_range|
376
+ v_range = value_range(x_range)
377
+ ar = @constellation.AllowedRange(v_range, role.role_value_restriction)
378
+ }
379
+ }
380
+
381
+ # puts "Adding Role #{role.name} to #{fact_type.name}"
382
+ #fact_type.add_role(role)
383
+ # puts "\tRole #{role} is #{id}"
384
+ }
385
+
386
+ # Deal with Readings:
387
+ x_reading_orders.each{|x|
388
+ x_role_sequence = x.elements.to_a('orm:RoleSequence/*')
389
+ x_readings = x.elements.to_a('orm:Readings/orm:Reading/orm:Data')
390
+
391
+ # Build an array of the Roles needed:
392
+ role_array = x_role_sequence.map{|x| @by_id[x.attributes['ref']] }
393
+
394
+ # puts "Reading #{x_readings.map(&:text).inspect}"
395
+ role_sequence = get_role_sequence(role_array)
396
+
397
+ #role_sequence.all_role_ref.each_with_index{|rr, i|
398
+ # # REVISIT: rr.leading_adjective = ...; Add adjectives here
399
+ # }
400
+
401
+ x_readings.each_with_index{|x, i|
402
+ reading = @constellation.Reading(fact_type, fact_type.all_reading.size)
403
+ reading.role_sequence = role_sequence
404
+ # REVISIT: The downcase here only needs to be the initial letter of each word, but be safe:
405
+ reading.reading_text = extract_adjectives(x.text, role_sequence).downcase
406
+ }
407
+ }
408
+ }
409
+ # @vocabulary.fact_types.each{|ft| puts ft }
410
+ end
411
+
412
+ def extract_adjectives(reading_text, role_sequence)
413
+ (0...role_sequence.all_role_ref.size).each{|i|
414
+ role_ref = role_sequence.all_role_ref[i]
415
+ role = role_ref.role
416
+
417
+ word = '\b([A-Za-z][A-Za-z0-9_]*)\b'
418
+ leading_adjectives_re = "(?:#{word}- *(?:#{word} +)?)"
419
+ trailing_adjectives_re = "(?: +(?:#{word} +) *-#{word}?)"
420
+ role_with_adjectives_re =
421
+ %r| ?#{leading_adjectives_re}?\{#{i}\}#{trailing_adjectives_re}? ?|
422
+
423
+ reading_text.gsub!(role_with_adjectives_re) {
424
+ la = [[$1]*"", [$2]*""]*" ".gsub(/\s+/, ' ').sub(/\s+\Z/,'').strip
425
+ ta = [[$1]*"", [$2]*""]*" ".gsub(/\s+/, ' ').sub(/\A\s+/,'').strip
426
+ #puts "Setting leading adj #{la.inspect} from #{reading_text.inspect} for #{role_ref.role.concept.name}" if la != ""
427
+ # REVISIT: Dunno what's up here, but removing the "if" test makes this chuck exceptions:
428
+ role_ref.leading_adjective = la if la != ""
429
+ role_ref.trailing_adjective = ta if ta != ""
430
+
431
+ #puts "Reading '#{reading_text}' has role #{i} adjectives '#{la}' '#{ta}'" if la != "" || ta != ""
432
+
433
+ " {#{i}} "
434
+ }
435
+ }
436
+ reading_text.sub!(/\A /, '')
437
+ reading_text.sub!(/ \Z/, '')
438
+ reading_text
439
+ end
440
+
441
+ def get_role_sequence(role_array)
442
+ # puts "Getting RoleSequence [#{role_array.map{|r| "#{r.concept.name} (role #{r.object_id})" }*", "}]"
443
+
444
+ # Look for an existing RoleSequence
445
+ # REVISIT: This searches all role sequences. Perhaps we could narrow it down first instead?
446
+ role_sequence = @constellation.RoleSequence.values.detect{|c|
447
+ #puts "Checking RoleSequence [#{c.all_role_ref.map{|rr| rr.role.concept.name}*", "}]"
448
+ role_array == c.all_role_ref.map{|rr| rr.role }
449
+ }
450
+ # puts "Found matching RoleSequence!" if role_sequence
451
+ return role_sequence if role_sequence
452
+
453
+ # Make a new RoleSequence:
454
+ role_sequence = @constellation.RoleSequence(:new) unless role_sequence
455
+ role_array.each_with_index{|r, i|
456
+ role_ref = @constellation.RoleRef(role_sequence, i)
457
+ role_ref.role = r
458
+ }
459
+ role_sequence
460
+ end
461
+
462
+ def map_roles(x_roles, why = nil)
463
+ role_array = x_roles.map{|x|
464
+ id = x.attributes['ref']
465
+ role = @by_id[id]
466
+ if (why && !role)
467
+ # We didn't make Implied objects, so some constraints are unconnectable
468
+ x_role = @x_by_id[id]
469
+ x_player = x_role.elements.to_a('orm:RolePlayer')[0]
470
+ x_object = @x_by_id[x_player.attributes['ref']]
471
+ x_nests = nil
472
+ if (x_object.name.to_s == 'ObjectifiedType')
473
+ x_nests = x_object.elements.to_a('orm:NestedPredicate')[0]
474
+ implied = x_nests.attributes['IsImplied']
475
+ x_fact = @x_by_id[x_nests.attributes['ref']]
476
+ end
477
+
478
+ # This might have been a role of an ImpliedFact, which makes it safe to ignore.
479
+ next if 'ImpliedFact' == x_role.parent.parent.name
480
+
481
+ # Talk about why this wasn't found - this shouldn't happen.
482
+ if (!x_nests || !implied)
483
+ #puts "="*60
484
+ puts "Skipping #{why}, #{x_role.name} #{id} not found"
485
+
486
+ if (x_nests)
487
+ puts "Role is on #{implied ? "implied " : ""}objectification #{x_object}"
488
+ puts "which objectifies #{x_fact}"
489
+ end
490
+ puts x_object.to_s
491
+ end
492
+ end
493
+ role
494
+ }
495
+ role_array.include?(nil) ? nil : get_role_sequence(role_array)
496
+ end
497
+
498
+ def read_constraints
499
+ @constraints_by_rs = {}
500
+
501
+ read_mandatory_constraints
502
+ read_uniqueness_constraints
503
+ read_exclusion_constraints
504
+ read_subset_constraints
505
+ read_ring_constraints
506
+ read_equality_constraints
507
+ read_frequency_constraints
508
+ read_residual_mandatory_constraints
509
+ end
510
+
511
+ def read_mandatory_constraints
512
+ x_mandatory_constraints = @x_model.elements.to_a("orm:Constraints/orm:MandatoryConstraint")
513
+ @mandatory_constraints_by_rs = {}
514
+ @mandatory_constraint_rs_by_id = {}
515
+ x_mandatory_constraints.each{|x|
516
+ name = x.attributes["Name"] || ''
517
+ name.gsub!(/\s/,'')
518
+ name = nil if name.size == 0
519
+
520
+ # As of Feb 2008, all NORMA ValueTypes have an implied mandatory constraint.
521
+ if x.elements.to_a("orm:ImpliedByObjectType").size > 0
522
+ # $stderr.puts "Skipping ImpliedMandatoryConstraint #{name} over #{roles}"
523
+ next
524
+ end
525
+
526
+ x_roles = x.elements.to_a("orm:RoleSequence/orm:Role")
527
+ roles = map_roles(x_roles, "mandatory constraint #{name}")
528
+ next if !roles
529
+
530
+ # If X-OR mandatory, the Exclusion is accessed by:
531
+ # x_exclusion = (ex = x.elements.to_a("orm:ExclusiveOrExclusionConstraint")[0]) &&
532
+ # @x_by_id[ex.attributes['ref']]
533
+ # puts "Mandatory #{name}(#{roles}) is paired with exclusive #{x_exclusion.attributes['Name']}" if x_exclusion
534
+
535
+ @mandatory_constraints_by_rs[roles] = x
536
+ @mandatory_constraint_rs_by_id[x.attributes['id']] = roles
537
+ }
538
+ end
539
+
540
+ def read_residual_mandatory_constraints
541
+ @mandatory_constraints_by_rs.each { |roles, x|
542
+ # Create a simply-mandatory PresenceConstraint for each mandatory constraint
543
+ name = x.attributes["Name"] || ''
544
+ name.gsub!(/\s/,'')
545
+ name = nil if name.size == 0
546
+ #puts "Residual Mandatory #{name}: #{roles.to_s}"
547
+
548
+ pc = @constellation.PresenceConstraint(:new)
549
+ pc.vocabulary = @vocabulary
550
+ pc.name = name
551
+ pc.role_sequence = roles
552
+ pc.is_mandatory = true
553
+ pc.min_frequency = 1
554
+ pc.max_frequency = nil
555
+ pc.is_preferred_identifier = false
556
+
557
+ (@constraints_by_rs[roles] ||= []) << pc
558
+ }
559
+ end
560
+
561
+ def read_uniqueness_constraints
562
+ x_uniqueness_constraints = @x_model.elements.to_a("orm:Constraints/orm:UniquenessConstraint")
563
+ x_uniqueness_constraints.each{|x|
564
+ name = x.attributes["Name"] || ''
565
+ name.gsub!(/\s/,'')
566
+ name = nil if name.size == 0
567
+ id = x.attributes["id"]
568
+ x_pi = x.elements.to_a("orm:PreferredIdentifierFor")[0]
569
+ pi = x_pi ? @by_id[eref = x_pi.attributes['ref']] : nil
570
+
571
+ # Skip uniqueness constraints on implied concepts
572
+ if x_pi && !pi
573
+ puts "Skipping uniqueness constraint #{name}, entity not found"
574
+ next
575
+ end
576
+
577
+ # A uniqueness constraint on a fact having an implied objectification isn't preferred:
578
+ # if pi &&
579
+ # (x_pi_for = @x_by_id[eref]) &&
580
+ # (np = x_pi_for.elements.to_a('orm:NestedPredicate')[0]) &&
581
+ # np.attributes['IsImplied']
582
+ # pi = nil
583
+ # end
584
+
585
+ # Get the RoleSequence:
586
+ x_roles = x.elements.to_a("orm:RoleSequence/orm:Role")
587
+ next if x_roles.size == 0
588
+ roles = map_roles(x_roles, "uniqueness constraint #{name}")
589
+ next if !roles
590
+
591
+ # There is an implicit uniqueness constraint when any object plays a unary. Skip it.
592
+ if (x_roles.size == 1 &&
593
+ (id = x_roles[0].attributes['ref']) &&
594
+ (x_role = @x_by_id[id]) &&
595
+ x_role.parent.elements.size == 2 &&
596
+ (sibling = x_role.parent.elements[2]) &&
597
+ (ib_id = sibling.elements[1].attributes['ref']) &&
598
+ (ib = @x_by_id[ib_id]) &&
599
+ ib.attributes['IsImplicitBooleanValue'])
600
+ unary_identifier = true
601
+ end
602
+
603
+ if (mc = @mandatory_constraints_by_rs[roles])
604
+ # Remove absorbed mandatory constraints, leaving residual ones.
605
+ # puts "Absorbing MC #{mc.attributes['Name']}"
606
+ @mandatory_constraints_by_rs.delete(roles)
607
+ @mandatory_constraint_rs_by_id.delete(mc.attributes['id'])
608
+ end
609
+
610
+ # A UC that spans more than one Role of a fact will be a Preferred Id for the implied object
611
+ #puts "Unique" + rs.to_s +
612
+ # (pi ? " (preferred id for #{pi.name})" : "") +
613
+ # (mc ? " (mandatory)" : "") if pi && !mc
614
+
615
+ # A TypeInheritance fact type has a UC on each role.
616
+ # If it's on the identification path, mark it as preferred identifier
617
+ role = roles.all_role_ref[0].role
618
+ supertype_constraint = role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
619
+ role.concept == role.fact_type.supertype &&
620
+ role.fact_type.provides_identification
621
+
622
+ pc = @constellation.PresenceConstraint(:new)
623
+ pc.vocabulary = @vocabulary
624
+ pc.name = name
625
+ pc.role_sequence = roles
626
+ pc.is_mandatory = true if mc
627
+ pc.min_frequency = mc ? 1 : 0
628
+ pc.max_frequency = 1
629
+ pc.is_preferred_identifier = true if pi || unary_identifier || supertype_constraint
630
+ #puts "#{name} covers #{roles.describe} has min=#{pc.min_frequency}, max=1, preferred=#{pc.is_preferred_identifier.inspect}" if emit_special_debug
631
+
632
+ #puts roles.verbalise
633
+ #puts pc.verbalise
634
+
635
+ (@constraints_by_rs[roles] ||= []) << pc
636
+ }
637
+ end
638
+
639
+ def read_exclusion_constraints
640
+ x_exclusion_constraints = @x_model.elements.to_a("orm:Constraints/orm:ExclusionConstraint")
641
+ x_exclusion_constraints.each{|x|
642
+ name = x.attributes["Name"] || ''
643
+ name.gsub!(/\s/,'')
644
+ name = nil if name.size == 0
645
+ x_mandatory = (m = x.elements.to_a("orm:ExclusiveOrMandatoryConstraint")[0]) &&
646
+ @x_by_id[mc_id = m.attributes['ref']]
647
+ role_sequences =
648
+ x.elements.to_a("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
649
+ x_role_refs = x_rs.elements.to_a("orm:Role")
650
+ map_roles(
651
+ x_role_refs , # .map{|xr| @x_by_id[xr.attributes['ref']] },
652
+ "exclusion constraint #{name}"
653
+ )
654
+ }
655
+ if x_mandatory
656
+ # Remove absorbed mandatory constraints, leaving residual ones.
657
+ mc_rs = @mandatory_constraint_rs_by_id[mc_id]
658
+ @mandatory_constraint_rs_by_id.delete(mc_id)
659
+ @mandatory_constraints_by_rs.delete(mc_rs)
660
+ end
661
+
662
+ ec = @constellation.SetExclusionConstraint(:new)
663
+ ec.vocabulary = @vocabulary
664
+ ec.name = name
665
+ # ec.enforcement =
666
+ role_sequences.each{|rs|
667
+ @constellation.SetComparisonRoles(ec, rs)
668
+ }
669
+ ec.is_mandatory = true if x_mandatory
670
+ }
671
+ end
672
+
673
+ def read_equality_constraints
674
+ x_equality_constraints = @x_model.elements.to_a("orm:Constraints/orm:EqualityConstraint")
675
+ x_equality_constraints.each{|x|
676
+ name = x.attributes["Name"] || ''
677
+ name.gsub!(/\s/,'')
678
+ name = nil if name.size == 0
679
+ role_sequences =
680
+ x.elements.to_a("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
681
+ x_role_refs = x_rs.elements.to_a("orm:Role")
682
+ map_roles(
683
+ x_role_refs , # .map{|xr| @x_by_id[xr.attributes['ref']] },
684
+ "equality constraint #{name}"
685
+ )
686
+ }
687
+
688
+ ec = @constellation.SetEqualityConstraint(:new)
689
+ ec.vocabulary = @vocabulary
690
+ ec.name = name
691
+ # ec.enforcement =
692
+ role_sequences.each{|rs|
693
+ @constellation.SetComparisonRoles(ec, rs)
694
+ }
695
+ }
696
+ end
697
+
698
+ def read_subset_constraints
699
+ x_subset_constraints = @x_model.elements.to_a("orm:Constraints/orm:SubsetConstraint")
700
+ x_subset_constraints.each{|x|
701
+ name = x.attributes["Name"] || ''
702
+ name.gsub!(/\s/,'')
703
+ name = nil if name.size == 0
704
+ role_sequences =
705
+ x.elements.to_a("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
706
+ x_role_refs = x_rs.elements.to_a("orm:Role")
707
+ map_roles(
708
+ x_role_refs , # .map{|xr| @x_by_id[xr.attributes['ref']] },
709
+ "equality constraint #{name}"
710
+ )
711
+ }
712
+
713
+ ec = @constellation.SubsetConstraint(:new)
714
+ ec.vocabulary = @vocabulary
715
+ ec.name = name
716
+ # ec.enforcement =
717
+ ec.subset_role_sequence = role_sequences[0]
718
+ ec.superset_role_sequence = role_sequences[1]
719
+ }
720
+ end
721
+
722
+ def read_ring_constraints
723
+ x_ring_constraints = @x_model.elements.to_a("orm:Constraints/orm:RingConstraint")
724
+ x_ring_constraints.each{|x|
725
+ name = x.attributes["Name"] || ''
726
+ name.gsub!(/\s/,'')
727
+ name = nil if name.size == 0
728
+ type = x.attributes["Type"]
729
+ # begin
730
+ # # Convert the RingConstraint name to a number:
731
+ # type_num = eval("::ActiveFacts::RingConstraint::#{type}")
732
+ # rescue => e
733
+ # throw "RingConstraint type #{type} isn't known"
734
+ # end
735
+
736
+ from, to = *x.elements.to_a("orm:RoleSequence/orm:Role").map{|xr|
737
+ @by_id[xr.attributes['ref']]
738
+ }
739
+ rc = @constellation.RingConstraint(:new)
740
+ rc.vocabulary = @vocabulary
741
+ rc.name = name
742
+ # rc.enforcement =
743
+ rc.role = from
744
+ rc.other_role = to
745
+ rc.ring_type = type
746
+ }
747
+ end
748
+
749
+ def read_frequency_constraints
750
+ x_frequency_constraints = @x_model.elements.to_a("orm:Constraints/orm:FrequencyConstraint")
751
+ # REVISIT: FrequencyConstraints not handled yet
752
+ end
753
+
754
+ def read_instances
755
+ population = Population.new(@vocabulary, "sample")
756
+
757
+ # Value instances first, then entities then facts:
758
+
759
+ x_values = @x_model.elements.to_a("orm:Objects/orm:ValueType/orm:Instances/orm:ValueTypeInstance/orm:Value")
760
+ #pp x_values.map{|v| [ v.parent.attributes['id'], v.text ] }
761
+ x_values.each{|v|
762
+ id = v.parent.attributes['id']
763
+ # Get details of the ValueType:
764
+ xvt = v.parent.parent.parent
765
+ vt_id = xvt.attributes['id']
766
+ vtname = xvt.attributes['Name'] || ''
767
+ vtname.gsub!(/\s/,'')
768
+ vtname = nil if name.size == 0
769
+ vt = @by_id[vt_id]
770
+ throw "ValueType #{vtname} not found" unless vt
771
+
772
+ i = Instance.new(vt, v.text)
773
+ @by_id[id] = i
774
+ # show_xmlobj(v)
775
+ }
776
+
777
+ # Use the "id" attribute of EntityTypeInstance
778
+ x_entities = @x_model.elements.to_a("orm:Objects/orm:EntityType/orm:Instances/orm:EntityTypeInstance")
779
+ #pp x_entities
780
+ # x_entities.each{|v| show_xmlobj(v) }
781
+ last_et_id = nil
782
+ last_et = nil
783
+ et = nil
784
+ x_entities.each{|v|
785
+ id = v.attributes['id']
786
+
787
+ # Get details of the EntityType:
788
+ xet = v.parent.parent
789
+ et_id = xet.attributes['id']
790
+ if (et_id != last_et_id)
791
+ etname = xet.attributes['Name'] || ''
792
+ etname.gsub!(/\s/,'')
793
+ etname = nil if name.size == 0
794
+ last_et = et = @by_id[et_id]
795
+ last_et_id = et_id
796
+ throw "EntityType #{etname} not found" unless et
797
+ end
798
+
799
+ instance = Instance.new(et)
800
+ @by_id[id] = instance
801
+ # puts "Made new EntityType #{etname}"
802
+ }
803
+
804
+ # The EntityType instances have implicit facts for the PI facts.
805
+ # We must create implicit PI facts after all the instances.
806
+ entity_count = 0
807
+ pi_fact_count = 0
808
+ x_entities.each{|v|
809
+ id = v.attributes['id']
810
+ instance = @by_id[id]
811
+ et = @by_id[v.parent.parent.attributes['id']]
812
+ next unless (preferred_id = et.preferred_identifier)
813
+
814
+ # puts "Create identifying facts using #{preferred_id}"
815
+
816
+ # Collate the referenced objects by role:
817
+ role_instances = v.elements[1].elements.inject({}){|h, v|
818
+ etri = @x_by_id[v.attributes['ref']]
819
+ x_role_id = etri.parent.parent.attributes['id']
820
+ role = @by_id[x_role_id]
821
+ object = @by_id[object_id = etri.attributes['ref']]
822
+ h[role] = object
823
+ h
824
+ }
825
+
826
+ # Create an instance of each required fact type, for compound identification:
827
+ preferred_id.role_sequence.map(&:fact_type).uniq.each{|ft|
828
+ # puts "\tFor FactType #{ft}"
829
+ fact_roles = ft.roles.map{|role|
830
+ if role.concept == et
831
+ object = instance
832
+ else
833
+ object = role_instances[role]
834
+ # puts "\t\tinstance for role #{role} is #{object}"
835
+ end
836
+ FactRole.new(role, object)
837
+ }
838
+ f = Fact.new(population, ft, *fact_roles)
839
+ pi_fact_count += 1
840
+ }
841
+ entity_count += 1
842
+ }
843
+ # puts "Created #{pi_fact_count} facts to identify #{entity_count} entities"
844
+
845
+ # Use the "ref" attribute of FactTypeRoleInstance:
846
+ x_fact_roles = @x_model.elements.to_a("orm:Facts/orm:Fact/orm:Instances/orm:FactTypeInstance/orm:RoleInstances/orm:FactTypeRoleInstance")
847
+
848
+ last_id = nil
849
+ last_fact_type = nil
850
+ fact_roles = []
851
+ x_fact_roles.each{|v|
852
+ fact_type_id = v.parent.parent.parent.parent.attributes['id']
853
+ id = v.parent.parent.attributes['id']
854
+ fact_type = @by_id[fact_type_id]
855
+ throw "Fact type #{fact_type_id} not found" unless fact_type
856
+
857
+ if (last_id && id != last_id)
858
+ # Process completed fact now we have all roles:
859
+ last_fact = Fact.new(population, last_fact_type, *fact_roles)
860
+ fact_roles = []
861
+ else
862
+ last_fact_type = fact_type
863
+ end
864
+
865
+ #show_xmlobj(v)
866
+
867
+ last_id = id
868
+ x_role_instance = @x_by_id[v.attributes['ref']]
869
+ x_role_id = x_role_instance.parent.parent.attributes['id']
870
+ role = @by_id[x_role_id]
871
+ throw "Role not found for instance #{x_role_id}" unless role
872
+ instance_id = x_role_instance.attributes['ref']
873
+ instance = @by_id[instance_id]
874
+ throw "Instance not found for FactRole #{instance_id}" unless instance
875
+ fact_roles << FactRole.new(role, instance)
876
+ }
877
+
878
+ if (last_id)
879
+ # Process final completed fact now we have all roles:
880
+ last_fact = Fact.new(population, last_fact_type, *fact_roles)
881
+ end
882
+
883
+ end
884
+
885
+ def read_rest
886
+ puts "Reading Implied Facts (not yet)"
887
+ =begin
888
+ x_implied_facts = @x_model.elements.to_a("orm:Facts/orm:ImpliedFact")
889
+ pp x_implied_facts
890
+ =end
891
+ puts "Reading Data Types (not yet)"
892
+ =begin
893
+ x_datatypes = @x_model.elements.to_a("orm:DataTypes/*")
894
+ pp x_datatypes
895
+ =end
896
+ puts "Reading Reference Mode Kinds (not yet)"
897
+ =begin
898
+ x_refmodekinds = @x_model.elements.to_a("orm:ReferenceModeKinds/*")
899
+ pp x_refmodekinds
900
+ =end
901
+ end
902
+
903
+ def show_xmlobj(x, indent = "")
904
+ parentage = []
905
+ p = x
906
+ while (p)
907
+ parentage.unshift(p)
908
+ p = p.parent
909
+ end
910
+ #parentage = parentage.shift
911
+ puts "#{indent}#{x.name} object has heritage {"
912
+ parentage.each{|p|
913
+ next if REXML::Document === p
914
+ puts "#{indent}\t#{p.name}#{
915
+ }#{(n = p.attributes['Name']) ? " Name='#{n}'" : ""
916
+ }#{(id = p.attributes['id']) ? " #{id}" : ""
917
+ }#{(ref = p.attributes['ref']) ? " -> #{ref}" : ""
918
+ }#{/\S/ === ((text = p.text)) ? " "+text.inspect : ""
919
+ }"
920
+ show_xmlobj(@x_by_id[ref], "\t#{indent}") if ref
921
+ }
922
+ puts "#{indent}}"
923
+ end
924
+ end
925
+ end
926
+ end