activefacts 0.6.0

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