activefacts 0.8.6 → 0.8.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/Manifest.txt +33 -2
  2. data/README.rdoc +30 -36
  3. data/Rakefile +16 -20
  4. data/bin/afgen +17 -11
  5. data/bin/cql +313 -36
  6. data/download.html +43 -19
  7. data/examples/CQL/Address.cql +15 -15
  8. data/examples/CQL/Blog.cql +8 -8
  9. data/examples/CQL/CompanyDirectorEmployee.cql +6 -5
  10. data/examples/CQL/Death.cql +3 -3
  11. data/examples/CQL/Diplomacy.cql +48 -0
  12. data/examples/CQL/Genealogy.cql +41 -41
  13. data/examples/CQL/Insurance.cql +311 -0
  14. data/examples/CQL/JoinEquality.cql +35 -0
  15. data/examples/CQL/Marriage.cql +1 -1
  16. data/examples/CQL/Metamodel.cql +290 -185
  17. data/examples/CQL/MetamodelNext.cql +420 -0
  18. data/examples/CQL/Monogamy.cql +24 -0
  19. data/examples/CQL/MonthInSeason.cql +27 -0
  20. data/examples/CQL/Moon.cql +23 -0
  21. data/examples/CQL/MultiInheritance.cql +4 -4
  22. data/examples/CQL/NonRoleId.cql +14 -0
  23. data/examples/CQL/OddIdentifier.cql +18 -0
  24. data/examples/CQL/OilSupply.cql +24 -24
  25. data/examples/CQL/OneToOnes.cql +17 -0
  26. data/examples/CQL/Orienteering.cql +55 -55
  27. data/examples/CQL/OrienteeringER.cql +58 -0
  28. data/examples/CQL/PersonPlaysGame.cql +2 -2
  29. data/examples/CQL/RedundantDependency.cql +34 -0
  30. data/examples/CQL/SchoolActivities.cql +5 -5
  31. data/examples/CQL/SeparateSubtype.cql +28 -0
  32. data/examples/CQL/ServiceDirector.cql +283 -0
  33. data/examples/CQL/SimplestUnary.cql +2 -2
  34. data/examples/CQL/SubtypePI.cql +11 -11
  35. data/examples/CQL/Supervision.cql +38 -0
  36. data/examples/CQL/Tests.Test5.Load.cql +38 -0
  37. data/examples/CQL/WaiterTips.cql +33 -0
  38. data/examples/CQL/Warehousing.cql +55 -53
  39. data/examples/CQL/WindowInRoomInBldg.cql +9 -9
  40. data/examples/CQL/unit.cql +433 -544
  41. data/examples/index.html +314 -170
  42. data/examples/intro.html +6 -176
  43. data/examples/local.css +8 -4
  44. data/index.html +40 -25
  45. data/lib/activefacts/api/concept.rb +2 -2
  46. data/lib/activefacts/api/constellation.rb +4 -4
  47. data/lib/activefacts/api/instance.rb +2 -2
  48. data/lib/activefacts/api/instance_index.rb +4 -0
  49. data/lib/activefacts/api/numeric.rb +3 -1
  50. data/lib/activefacts/api/role.rb +1 -1
  51. data/lib/activefacts/api/standard_types.rb +23 -16
  52. data/lib/activefacts/api/support.rb +3 -1
  53. data/lib/activefacts/api/vocabulary.rb +4 -0
  54. data/lib/activefacts/cql/CQLParser.treetop +87 -39
  55. data/lib/activefacts/cql/Concepts.treetop +95 -69
  56. data/lib/activefacts/cql/Context.treetop +11 -2
  57. data/lib/activefacts/cql/Expressions.treetop +23 -59
  58. data/lib/activefacts/cql/FactTypes.treetop +141 -95
  59. data/lib/activefacts/cql/Language/English.treetop +33 -21
  60. data/lib/activefacts/cql/LexicalRules.treetop +6 -1
  61. data/lib/activefacts/cql/Terms.treetop +75 -26
  62. data/lib/activefacts/cql/ValueTypes.treetop +52 -54
  63. data/lib/activefacts/cql/compiler.rb +46 -1691
  64. data/lib/activefacts/cql/compiler/constraint.rb +602 -0
  65. data/lib/activefacts/cql/compiler/entity_type.rb +425 -0
  66. data/lib/activefacts/cql/compiler/fact.rb +300 -0
  67. data/lib/activefacts/cql/compiler/fact_type.rb +230 -0
  68. data/lib/activefacts/cql/compiler/reading.rb +832 -0
  69. data/lib/activefacts/cql/compiler/shared.rb +109 -0
  70. data/lib/activefacts/cql/compiler/value_type.rb +104 -0
  71. data/lib/activefacts/cql/parser.rb +132 -81
  72. data/lib/activefacts/generate/cql.rb +397 -274
  73. data/lib/activefacts/generate/oo.rb +13 -12
  74. data/lib/activefacts/generate/ordered.rb +107 -117
  75. data/lib/activefacts/generate/ruby.rb +34 -38
  76. data/lib/activefacts/generate/sql/mysql.rb +62 -45
  77. data/lib/activefacts/generate/sql/server.rb +59 -42
  78. data/lib/activefacts/input/cql.rb +6 -3
  79. data/lib/activefacts/input/orm.rb +991 -557
  80. data/lib/activefacts/persistence/columns.rb +16 -12
  81. data/lib/activefacts/persistence/foreignkey.rb +7 -4
  82. data/lib/activefacts/persistence/index.rb +3 -4
  83. data/lib/activefacts/persistence/reference.rb +5 -2
  84. data/lib/activefacts/support.rb +20 -14
  85. data/lib/activefacts/version.rb +1 -1
  86. data/lib/activefacts/vocabulary.rb +1 -0
  87. data/lib/activefacts/vocabulary/extensions.rb +328 -44
  88. data/lib/activefacts/vocabulary/metamodel.rb +145 -20
  89. data/lib/activefacts/vocabulary/verbaliser.rb +621 -0
  90. data/spec/absorption_spec.rb +4 -4
  91. data/spec/api/value_type.rb +1 -1
  92. data/spec/cql/context_spec.rb +45 -22
  93. data/spec/cql/deontic_spec.rb +88 -0
  94. data/spec/cql/matching_spec.rb +517 -0
  95. data/spec/cql/samples_spec.rb +88 -31
  96. data/spec/cql/unit_spec.rb +58 -37
  97. data/spec/cql_cql_spec.rb +12 -7
  98. data/spec/cql_mysql_spec.rb +3 -7
  99. data/spec/cql_parse_spec.rb +0 -4
  100. data/spec/cql_ruby_spec.rb +1 -4
  101. data/spec/cql_sql_spec.rb +5 -18
  102. data/spec/cql_symbol_tables_spec.rb +3 -0
  103. data/spec/cqldump_spec.rb +0 -2
  104. data/spec/helpers/array_matcher.rb +35 -0
  105. data/spec/helpers/ctrl_c_support.rb +52 -0
  106. data/spec/helpers/diff_matcher.rb +38 -0
  107. data/spec/helpers/file_matcher.rb +5 -3
  108. data/spec/helpers/string_matcher.rb +39 -0
  109. data/spec/helpers/test_parser.rb +13 -0
  110. data/spec/norma_cql_spec.rb +13 -5
  111. data/spec/norma_ruby_spec.rb +11 -3
  112. data/spec/{absorption_ruby_spec.rb → norma_ruby_sql_spec.rb} +37 -32
  113. data/spec/norma_sql_spec.rb +11 -5
  114. data/spec/norma_tables_spec.rb +33 -29
  115. data/spec/spec_helper.rb +4 -1
  116. data/status.html +92 -23
  117. metadata +102 -36
  118. data/lib/activefacts/generate/cql/html.rb +0 -403
@@ -15,7 +15,6 @@ module ActiveFacts
15
15
  # afgen --sql/mysql[=options] <file>.cql
16
16
  # Options are comma or space separated:
17
17
  # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
18
- # * norma Translate valuetypes from NORMA to SQL Server
19
18
  class MYSQL
20
19
  private
21
20
  include Persistence
@@ -61,7 +60,6 @@ module ActiveFacts
61
60
  @vocabulary = vocabulary
62
61
  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
63
62
  @delay_fks = options.include? "delay_fks"
64
- @norma = options.include? "norma"
65
63
  end
66
64
 
67
65
  def puts s
@@ -82,56 +80,75 @@ module ActiveFacts
82
80
  end
83
81
  end
84
82
 
85
- # Return SQL type and (modified?) length for the passed NORMA base type
86
- def norma_type(type, length)
83
+ # Return SQL type and (modified?) length for the passed base type
84
+ def normalise_type(type, length)
87
85
  sql_type = case type
88
- when "AutoCounter"; "INT"
89
- when "SignedInteger",
90
- "SignedSmallInteger"
86
+ when /^Auto ?Counter$/
87
+ 'int'
88
+
89
+ when /^Signed ?Integer$/,
90
+ /^Signed ?Small ?Integer$/
91
91
  s = case
92
- when length <= 8; "TINYINT UNSIGNED"
93
- when length <= 16; "SMALLINT UNSIGNED"
94
- when length <= 24; "MEDIUMINT UNSIGNED"
95
- else "INT UNSIGNED"
92
+ when length <= 8
93
+ 'tinyint'
94
+ when length <= 16
95
+ 'shortint'
96
+ when length <= 32
97
+ 'int'
98
+ else 'bigint'
96
99
  end
97
100
  length = nil
98
101
  s
99
- when "UnsignedInteger",
100
- "UnsignedSmallInteger",
101
- "UnsignedTinyInteger"
102
+
103
+ when /^Unsigned ?Integer$/,
104
+ /^Unsigned ?Small ?Integer$/,
105
+ /^Unsigned ?Tiny ?Integer$/
102
106
  s = case
103
- when length <= 8; "TINYINT"
104
- when length <= 16; "SMALLINT"
105
- when length <= 24; "MEDIUMINT"
106
- when length <= 32; "INT"
107
- else "BIGINT"
107
+ when length <= 8
108
+ 'tinyint unsigned'
109
+ when length <= 16
110
+ 'shortint unsigned'
111
+ when length <= 32
112
+ 'int unsigned'
113
+ else 'bigint'
108
114
  end
109
115
  length = nil
110
116
  s
111
- when "Decimal"; "DECIMAL"
112
117
 
113
- when "FixedLengthText";
118
+ when /^Decimal$/
119
+ 'decimal'
120
+
121
+ when /^Fixed ?Length ?Text$/, /^Char$/
114
122
  length ||= DefaultCharColLength
115
- "CHAR"
116
- when "VariableLengthText";
123
+ "char"
124
+ when /^Variable ?Length ?Text$/, /^String$/
117
125
  length ||= DefaultCharColLength
118
- "VARCHAR"
126
+ "varchar"
119
127
  # There are several large length text types; If you need to store more than 65k chars, look at using MEDIUMTEXT or LONGTEXT
120
128
  # CQL does not yet allow you to specify a length for LargeLengthText.
121
- when "LargeLengthText"; "TEXT"
129
+ when /^Large ?Length ?Text$/, /^Text$/
130
+ 'text'
122
131
 
123
- when "DateAndTime"; "DATETIME"
124
- when "Date"; "DATE"
125
- when "Time"; "TIME"
126
- when "AutoTimestamp"; "TIMESTAMP"
132
+ when /^Date ?And ?Time$/, /^Date ?Time$/
133
+ 'datetime'
134
+ when /^Date$/
135
+ 'date'
136
+ when /^Time$/
137
+ 'time'
138
+ when /^Auto ?Time ?Stamp$/
139
+ 'timestamp'
127
140
 
128
- when "Money"; "DECIMAL"
141
+ when /^Money$/
142
+ 'decimal'
129
143
  # Warning: Max 65 kbytes. To use larger types, try MediumBlob (16mb) or LongBlob (4gb)
130
- when "PictureRawData"; "BLOB"
131
- when "VariableLengthRawData"; "BLOB"
144
+ when /^Picture ?Raw ?Data$/, /^Image$/
145
+ 'blob'
146
+ when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
147
+ 'blob'
132
148
  # Assuming you only want a boolean out of this. Should we specify length instead?
133
- when "BIT"; "BIT"
134
- else raise "SQL type unknown for NORMA type #{type}"
149
+ when /^BIT$/
150
+ 'bit'
151
+ else type # raise "SQL type unknown for standard type #{type}"
135
152
  end
136
153
  [sql_type, length]
137
154
  end
@@ -145,7 +162,7 @@ module ActiveFacts
145
162
  delayed_foreign_keys = []
146
163
 
147
164
  @vocabulary.tables.each do |table|
148
- puts "CREATE TABLE #{escape table.name} ("
165
+ puts "CREATE TABLE #{escape table.name.gsub(' ','')} ("
149
166
 
150
167
  pk = table.identifier_columns
151
168
  identity_column = pk[0] if pk.size == 1 && pk[0].is_auto_assigned
@@ -160,13 +177,13 @@ module ActiveFacts
160
177
  columns = table.columns.sort_by { |column| column.name(nil) }.map do |column|
161
178
  name = escape column.name("")
162
179
  padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
163
- type, params, restrictions = column.type
164
- restrictions = [] if (fk_columns.include?(column)) # Don't enforce VT restrictions on FK columns
180
+ type, params, constraints = column.type
181
+ constraints = [] if (fk_columns.include?(column)) # Don't enforce VT constraints on FK columns
165
182
  length = params[:length]
166
183
  length &&= length.to_i
167
184
  scale = params[:scale]
168
185
  scale &&= scale.to_i
169
- type, length = norma_type(type, length) if @norma
186
+ type, length = normalise_type(type, length)
170
187
  sql_type = "#{type}#{
171
188
  if !length
172
189
  ""
@@ -176,7 +193,7 @@ module ActiveFacts
176
193
  }"
177
194
  identity = column == identity_column ? " AUTO_INCREMENT" : ""
178
195
  null = (column.is_mandatory ? "NOT " : "") + "NULL"
179
- check = check_clause(name, restrictions)
196
+ check = check_clause(name, constraints)
180
197
  comment = column.comment
181
198
  [ "-- #{comment}", "#{name}#{padding}#{sql_type}#{identity} #{null}#{check}" ]
182
199
  end.flatten
@@ -189,7 +206,7 @@ module ActiveFacts
189
206
  table.foreign_keys.each do |fk|
190
207
  fk_text = "FOREIGN KEY (" +
191
208
  fk.from_columns.map{|column| column.name}*", " +
192
- ") REFERENCES #{escape fk.to.name} (" +
209
+ ") REFERENCES #{escape fk.to.name.gsub(' ','')} (" +
193
210
  fk.to_columns.map{|column| column.name}*", " +
194
211
  ")"
195
212
  if !@delay_fks and # We don't want to delay all Fks
@@ -197,7 +214,7 @@ module ActiveFacts
197
214
  fk.to == table && !fk.to_columns.detect{|column| !column.is_mandatory}) # The reference columns already have the required indexes
198
215
  inline_fks << fk_text
199
216
  else
200
- delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name}\n\tADD " + fk_text)
217
+ delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name.gsub(' ','')}\n\tADD " + fk_text)
201
218
  end
202
219
  end
203
220
 
@@ -235,11 +252,11 @@ module ActiveFacts
235
252
  "'" + str.gsub(/'/,"''") + "'"
236
253
  end
237
254
 
238
- def check_clause(column_name, restrictions)
239
- return "" if restrictions.empty?
240
- # REVISIT: Merge all restrictions (later; now just use the first)
255
+ def check_clause(column_name, constraints)
256
+ return "" if constraints.empty?
257
+ # REVISIT: Merge all constraints (later; now just use the first)
241
258
  " CHECK(" +
242
- restrictions[0].all_allowed_range_sorted.map do |ar|
259
+ constraints[0].all_allowed_range_sorted.map do |ar|
243
260
  vr = ar.value_range
244
261
  min = vr.minimum_bound
245
262
  max = vr.maximum_bound
@@ -15,7 +15,6 @@ module ActiveFacts
15
15
  # afgen --sql/server[=options] <file>.cql
16
16
  # Options are comma or space separated:
17
17
  # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
18
- # * norma Translate valuetypes from NORMA to SQL Server
19
18
  class SERVER
20
19
  private
21
20
  include Persistence
@@ -48,7 +47,6 @@ module ActiveFacts
48
47
  @vocabulary = vocabulary
49
48
  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
50
49
  @delay_fks = options.include? "delay_fks"
51
- @norma = options.include? "norma"
52
50
  @underscore = options.include?("underscore") ? "_" : ""
53
51
  end
54
52
 
@@ -71,39 +69,58 @@ module ActiveFacts
71
69
  end
72
70
  end
73
71
 
74
- # Return SQL type and (modified?) length for the passed NORMA base type
75
- def norma_type(type, length)
72
+ # Return SQL type and (modified?) length for the passed base type
73
+ def normalise_type(type, length)
76
74
  sql_type = case type
77
- when "AutoCounter"; "int"
78
- when "UnsignedInteger",
79
- "SignedInteger",
80
- "UnsignedSmallInteger",
81
- "SignedSmallInteger",
82
- "UnsignedTinyInteger"
75
+ when /^Auto ?Counter$/
76
+ 'int'
77
+
78
+ when /^Unsigned ?Integer$/,
79
+ /^Signed ?Integer$/,
80
+ /^Unsigned ?Small ?Integer$/,
81
+ /^Signed ?Small ?Integer$/,
82
+ /^Unsigned ?Tiny ?Integer$/
83
83
  s = case
84
- when length <= 8; "tinyint"
85
- when length <= 16; "shortint"
86
- when length <= 32; "int"
87
- else "bigint"
84
+ when length <= 8
85
+ 'tinyint'
86
+ when length <= 16
87
+ 'shortint'
88
+ when length <= 32
89
+ 'int'
90
+ else
91
+ 'bigint'
88
92
  end
89
93
  length = nil
90
94
  s
91
- when "Decimal"; "decimal"
92
-
93
- when "FixedLengthText"; "char"
94
- when "VariableLengthText"; "varchar"
95
- when "LargeLengthText"; "text"
96
-
97
- when "DateAndTime"; "datetime"
98
- when "Date"; "datetime" # SQLSVR 2K5: "date"
99
- when "Time"; "datetime" # SQLSVR 2K5: "time"
100
- when "AutoTimestamp"; "timestamp"
101
-
102
- when "Money"; "decimal"
103
- when "PictureRawData"; "image"
104
- when "VariableLengthRawData"; "varbinary"
105
- when "BIT"; "bit"
106
- else type # raise "SQL type unknown for NORMA type #{type}"
95
+
96
+ when /^Decimal$/
97
+ 'decimal'
98
+
99
+ when /^Fixed ?Length ?Text$/, /^Char$/
100
+ 'char'
101
+ when /^Variable ?Length ?Text$/, /^String$/
102
+ 'varchar'
103
+ when /^Large ?Length ?Text$/, /^Text$/
104
+ 'text'
105
+
106
+ when /^Date ?And ?Time$/, /^Date ?Time$/
107
+ 'datetime'
108
+ when /^Date$/
109
+ 'datetime' # SQLSVR 2K5: 'date'
110
+ when /^Time$/
111
+ 'datetime' # SQLSVR 2K5: 'time'
112
+ when /^Auto ?Time ?Stamp$/
113
+ 'timestamp'
114
+
115
+ when /^Money$/
116
+ 'decimal'
117
+ when /^Picture ?Raw ?Data$/, /^Image$/
118
+ 'image'
119
+ when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
120
+ 'varbinary'
121
+ when /^BIT$/
122
+ 'bit'
123
+ else type # raise "SQL type unknown for standard type #{type}"
107
124
  end
108
125
  [sql_type, length]
109
126
  end
@@ -117,7 +134,7 @@ module ActiveFacts
117
134
  delayed_foreign_keys = []
118
135
 
119
136
  @vocabulary.tables.each do |table|
120
- puts "CREATE TABLE #{escape table.name(@underscore)} ("
137
+ puts "CREATE TABLE #{escape table.name(@underscore).gsub(' ',@underscore)} ("
121
138
 
122
139
  pk = table.identifier_columns
123
140
  identity_column = pk[0] if pk.size == 1 && pk[0].is_auto_assigned
@@ -132,13 +149,13 @@ module ActiveFacts
132
149
  columns = table.columns.sort_by { |column| column.name(@underscore) }.map do |column|
133
150
  name = escape column.name(@underscore)
134
151
  padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
135
- type, params, restrictions = column.type
136
- restrictions = [] if (fk_columns.include?(column)) # Don't enforce VT restrictions on FK columns
152
+ type, params, constraints = column.type
153
+ constraints = [] if (fk_columns.include?(column)) # Don't enforce VT constraints on FK columns
137
154
  length = params[:length]
138
155
  length &&= length.to_i
139
156
  scale = params[:scale]
140
157
  scale &&= scale.to_i
141
- type, length = norma_type(type, length) if @norma
158
+ type, length = normalise_type(type, length)
142
159
  sql_type = "#{type}#{
143
160
  if !length
144
161
  ""
@@ -148,7 +165,7 @@ module ActiveFacts
148
165
  }"
149
166
  identity = column == identity_column ? " IDENTITY" : ""
150
167
  null = (column.is_mandatory ? "NOT " : "") + "NULL"
151
- check = check_clause(name, restrictions)
168
+ check = check_clause(name, constraints)
152
169
  comment = column.comment
153
170
  [ "-- #{comment}", "#{name}#{padding}#{sql_type}#{identity} #{null}#{check}" ]
154
171
  end.flatten
@@ -161,7 +178,7 @@ module ActiveFacts
161
178
  table.foreign_keys.each do |fk|
162
179
  fk_text = "FOREIGN KEY (" +
163
180
  fk.from_columns.map{|column| column.name(@underscore)}*", " +
164
- ") REFERENCES #{escape fk.to.name(@underscore)} (" +
181
+ ") REFERENCES #{escape fk.to.name(@underscore).gsub(' ',@underscore)} (" +
165
182
  fk.to_columns.map{|column| column.name(@underscore)}*", " +
166
183
  ")"
167
184
  if !@delay_fks and # We don't want to delay all Fks
@@ -169,7 +186,7 @@ module ActiveFacts
169
186
  fk.to == table && !fk.to_columns.detect{|column| !column.is_mandatory}) # The reference columns already have the required indexes
170
187
  inline_fks << fk_text
171
188
  else
172
- delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name(@underscore)}\n\tADD " + fk_text)
189
+ delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name(@underscore).gsub(' ',@underscore)}\n\tADD " + fk_text)
173
190
  end
174
191
  end
175
192
 
@@ -187,7 +204,7 @@ module ActiveFacts
187
204
  view_name = escape "#{index.view_name}_#{abbreviated_column_names}"
188
205
  delayed_indices <<
189
206
  %Q{CREATE VIEW dbo.#{view_name} (#{column_name_list}) WITH SCHEMABINDING AS
190
- \tSELECT #{column_name_list} FROM dbo.#{escape index.on.name(@underscore)}
207
+ \tSELECT #{column_name_list} FROM dbo.#{escape index.on.name(@underscore).gsub(' ',@underscore)}
191
208
  \tWHERE\t#{
192
209
  index.columns.
193
210
  select{|column| !column.is_mandatory }.
@@ -225,11 +242,11 @@ CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.c
225
242
  "'" + str.gsub(/'/,"''") + "'"
226
243
  end
227
244
 
228
- def check_clause(column_name, restrictions)
229
- return "" if restrictions.empty?
230
- # REVISIT: Merge all restrictions (later; now just use the first)
245
+ def check_clause(column_name, constraints)
246
+ return "" if constraints.empty?
247
+ # REVISIT: Merge all constraints (later; now just use the first)
231
248
  " CHECK(" +
232
- restrictions[0].all_allowed_range_sorted.map do |ar|
249
+ constraints[0].all_allowed_range_sorted.map do |ar|
233
250
  vr = ar.value_range
234
251
  min = vr.minimum_bound
235
252
  max = vr.maximum_bound
@@ -18,8 +18,10 @@ module ActiveFacts
18
18
  read(file, filename)
19
19
  }
20
20
  rescue => e
21
- puts e.message+"\n\t"+e.backtrace*"\n\t" if debug :exception
22
- raise "In #{filename} #{e.message.strip}"
21
+ # Augment the exception message, but preserve the backtrace
22
+ ne = StandardError.new("In #{filename} #{e.message.strip}")
23
+ ne.set_backtrace(e.backtrace)
24
+ raise ne
23
25
  end
24
26
 
25
27
  # Read the specified input stream
@@ -29,7 +31,8 @@ module ActiveFacts
29
31
 
30
32
  # Read the specified input string
31
33
  def self.readstring(str, filename = "string")
32
- ActiveFacts::CQL::Compiler.new(str, filename).vocabulary
34
+ compiler = ActiveFacts::CQL::Compiler.new(filename)
35
+ compiler.compile(str)
33
36
  end
34
37
  end
35
38
  end
@@ -31,20 +31,60 @@ module ActiveFacts
31
31
  # afgen --<generator> <file>.orm
32
32
  # This parser uses Rexml so it's very slow.
33
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" => "Integer(8)",
67
+ "ObjectIdOther" => "Integer(8)"
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
+
34
73
  private
35
- def self.readfile(filename)
74
+ def self.readfile(filename, *options)
36
75
  File.open(filename) {|file|
37
- self.read(file, filename)
76
+ self.read(file, filename, *options)
38
77
  }
39
78
  end
40
79
 
41
- def self.read(file, filename = "stdin")
42
- ORM.new(file, filename).read
80
+ def self.read(file, filename = "stdin", *options)
81
+ ORM.new(file, filename, *options).read
43
82
  end
44
83
 
45
- def initialize(file, filename = "stdin")
84
+ def initialize(file, filename = "stdin", *options)
46
85
  @file = file
47
86
  @filename = filename
87
+ @options = options
48
88
  end
49
89
 
50
90
  public
@@ -78,7 +118,7 @@ module ActiveFacts
78
118
  def read_vocabulary
79
119
  @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
80
120
  vocabulary_name = @x_model['Name']
81
- @vocabulary = @constellation.Vocabulary(@x_model['Name'])
121
+ @vocabulary = @constellation.Vocabulary(vocabulary_name)
82
122
 
83
123
  # Find all elements having an "id" attribute and index them
84
124
  x_identified = @x_model.xpath(".//*[@id]")
@@ -97,9 +137,10 @@ module ActiveFacts
97
137
  read_nested_types
98
138
  read_subtypes
99
139
  read_roles
140
+ complete_nested_types
100
141
  read_constraints
101
- # REVISIT: Skip instance data for now:
102
- #read_instances
142
+ read_instances if @options.include?("instances")
143
+ read_diagrams if @options.include?("diagrams")
103
144
  end
104
145
 
105
146
  def read_entity_types
@@ -108,8 +149,7 @@ module ActiveFacts
108
149
  x_entity_types = @x_model.xpath("orm:Objects/orm:EntityType")
109
150
  x_entity_types.each{|x|
110
151
  id = x['id']
111
- name = x['Name'] || ""
112
- name.gsub!(/\s/,'')
152
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
113
153
  name = nil if name.size == 0
114
154
  entity_types <<
115
155
  @by_id[id] =
@@ -134,8 +174,7 @@ module ActiveFacts
134
174
  #pp x_value_types
135
175
  x_value_types.each{|x|
136
176
  id = x['id']
137
- name = x['Name'] || ""
138
- name.gsub!(/\s/,'')
177
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
139
178
  name = nil if name.size == 0
140
179
 
141
180
  cdt = x.xpath('orm:ConceptualDataType')[0]
@@ -143,13 +182,17 @@ module ActiveFacts
143
182
  scale = scale != "" && scale.to_i
144
183
  length = cdt['Length']
145
184
  length = length != "" && length.to_i
185
+ length = nil if length <= 0
146
186
  base_type = @x_by_id[cdt['ref']]
147
187
  type_name = "#{base_type.name}"
148
188
  type_name.sub!(/^orm:/,'')
189
+
149
190
  type_name.sub!(/DataType\Z/,'')
150
- type_name.sub!(/Numeric\Z/,'')
151
- type_name.sub!(/Temporal\Z/,'')
152
- length = 32 if type_name =~ /Integer\Z/ && length.to_i == 0 # Set default integer length
191
+ type_name = DataTypeMapping[type_name] || type_name
192
+ if !length and type_name =~ /\(([0-9]+)\)/
193
+ length = $1.to_i
194
+ end
195
+ type_name = type_name.sub(/\(([0-9]*)\)/,'')
153
196
 
154
197
  # REVISIT: Need to handle standard types better here:
155
198
  value_super_type = type_name != name ? @constellation.ValueType(@vocabulary, type_name) : nil
@@ -159,18 +202,21 @@ module ActiveFacts
159
202
  vt = @constellation.ValueType(@vocabulary, name)
160
203
  vt.supertype = value_super_type
161
204
  vt.length = length if length
162
- vt.scale = scale if scale
205
+ vt.scale = scale if scale && scale != 0
163
206
  independent = x['IsIndependent']
164
207
  vt.is_independent = true if independent && independent == 'true'
165
208
  personal = x['IsPersonal']
166
209
  vt.pronoun = 'personal' if personal && personal == 'true'
167
210
 
168
- x_ranges = x.xpath("orm:ValueRestriction/orm:ValueConstraint/orm:ValueRanges/orm:ValueRange")
169
- next if x_ranges.size == 0
170
- vt.value_restriction = @constellation.ValueRestriction(:new)
171
- x_ranges.each{|x_range|
172
- v_range = value_range(x_range)
173
- ar = @constellation.AllowedRange(vt.value_restriction, v_range)
211
+ x_vr = x.xpath("orm:ValueRestriction/orm:ValueConstraint")
212
+ x_vr.each{|vr|
213
+ x_ranges = vr.xpath("orm:ValueRanges/orm:ValueRange")
214
+ next if x_ranges.size == 0
215
+ vt.value_constraint = @by_id[vr['id']] = @constellation.ValueConstraint(:new)
216
+ x_ranges.each{|x_range|
217
+ v_range = value_range(x_range)
218
+ ar = @constellation.AllowedRange(vt.value_constraint, v_range)
219
+ }
174
220
  }
175
221
  }
176
222
  end
@@ -191,17 +237,18 @@ module ActiveFacts
191
237
  # Handle the fact types:
192
238
  facts = []
193
239
  @x_facts = @x_model.xpath("orm:Facts/orm:Fact")
194
- @x_facts.each{|x|
195
- id = x['id']
196
- name = x['Name'] || x['_Name']
197
- name = "<unnamed>" if !name
198
- name.gsub!(/\s/,'')
199
- name = "" if !name || name.size == 0
200
- # Note that the new metamodel doesn't have a name for a facttype unless it's objectified
201
-
202
- # puts "FactType #{name || id}"
203
- facts << @by_id[id] = fact_type = @constellation.FactType(:new)
204
- }
240
+ debug :orm, "Reading fact types" do
241
+ @x_facts.each{|x|
242
+ id = x['id']
243
+ name = x['Name'] || x['_Name']
244
+ name = "<unnamed>" if !name
245
+ name = "" if !name || name.size == 0
246
+ # Note that the new metamodel doesn't have a name for a facttype unless it's objectified
247
+
248
+ debug :orm, "FactType #{name || id}"
249
+ facts << @by_id[id] = fact_type = @constellation.FactType(:new)
250
+ }
251
+ end
205
252
  end
206
253
 
207
254
  def read_subtypes
@@ -215,220 +262,207 @@ module ActiveFacts
215
262
  @x_mappings = []
216
263
  end
217
264
 
218
- @x_subtypes.each{|x|
219
- id = x['id']
220
- name = x['Name'] || x['_Name'] || ''
221
- name.gsub!(/\s/,'')
222
- name = nil if name.size == 0
223
- # puts "FactType #{name || id}"
224
-
225
- x_subtype_role = x.xpath('orm:FactRoles/orm:SubtypeMetaRole')[0]
226
- subtype_role_id = x_subtype_role['id']
227
- subtype_id = x_subtype_role.xpath('orm:RolePlayer')[0]['ref']
228
- subtype = @by_id[subtype_id]
229
- # REVISIT: Provide a way in the metamodel of handling Partition, (and mapping choices that vary for each supertype?)
230
-
231
- x_supertype_role = x.xpath('orm:FactRoles/orm:SupertypeMetaRole')[0]
232
- supertype_role_id = x_supertype_role['id']
233
- supertype_id = x_supertype_role.xpath('orm:RolePlayer')[0]['ref']
234
- supertype = @by_id[supertype_id]
235
-
236
- throw "For Subtype fact #{name}, the supertype #{supertype_id} was not found" if !supertype
237
- throw "For Subtype fact #{name}, the subtype #{subtype_id} was not found" if !subtype
238
- # $stderr.puts "#{subtype.name} is a subtype of #{supertype.name}"
239
-
240
- inheritance_fact = @constellation.TypeInheritance(subtype, supertype)
241
- inheritance_fact.fact_type_id = :new
242
- if x["IsPrimary"] == "true" or # Old way
243
- x["PreferredIdentificationPath"] == "true" # Newer
244
- # $stderr.puts "#{supertype.name} is primary supertype of #{subtype.name}"
245
- inheritance_fact.provides_identification = true
246
- end
247
- mapping = @x_mappings.detect{ |m| m['ref'] == id }
248
- mapping_choice = mapping ? mapping.parent['AbsorptionChoice'] : 'Absorbed'
249
- inheritance_fact.assimilation = mapping_choice.downcase.sub(/partition/, 'partitioned') if mapping_choice != 'Absorbed'
250
- facts << @by_id[id] = inheritance_fact
251
-
252
- # Create the new Roles so we can find constraints on them:
253
- subtype_role = @by_id[subtype_role_id] = @constellation.Role(inheritance_fact, 0, :concept => subtype)
254
- supertype_role = @by_id[supertype_role_id] = @constellation.Role(inheritance_fact, 1, :concept => supertype)
255
-
256
- # Create readings, so constraints can be verbalised for example:
257
- rs = @constellation.RoleSequence(:new)
258
- @constellation.RoleRef(rs, 0, :role => subtype_role)
259
- @constellation.RoleRef(rs, 1, :role => supertype_role)
260
- @constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :text => "{0} is a kind of {1}")
261
- @constellation.Reading(inheritance_fact, 1, :role_sequence => rs, :text => "{0} is a subtype of {1}")
262
-
263
- rs2 = @constellation.RoleSequence(:new)
264
- @constellation.RoleRef(rs2, 0, :role => supertype_role)
265
- @constellation.RoleRef(rs2, 1, :role => subtype_role)
266
- n = 'aeiouh'.include?(subtype_role.concept.name.downcase[0]) ? 1 : 0
267
- @constellation.Reading(inheritance_fact, 2+n, :role_sequence => rs2, :text => "{0} is a {1}")
268
- @constellation.Reading(inheritance_fact, 3-n, :role_sequence => rs2, :text => "{0} is an {1}")
269
-
270
- # The required uniqueness constraints are already present in the NORMA file, don't duplicate them
271
- =begin
272
- # Create uniqueness constraints over the subtyping fact type
273
- p1rs = @constellation.RoleSequence(:new)
274
- @constellation.RoleRef(p1rs, 0).role = subtype_role
275
- pc1 = @constellation.PresenceConstraint(:new)
276
- pc1.name = "#{subtype.name}MustHaveSupertype#{supertype.name}"
277
- pc1.vocabulary = @vocabulary
278
- pc1.role_sequence = p1rs
279
- pc1.is_mandatory = true # A subtype instance must have a supertype instance
280
- pc1.min_frequency = 1
281
- pc1.max_frequency = 1
282
- pc1.is_preferred_identifier = false
283
-
284
- # The supertype role often identifies the subtype:
285
- p2rs = @constellation.RoleSequence(:new)
286
- @constellation.RoleRef(p2rs, 0).role = supertype_role
287
- pc2 = @constellation.PresenceConstraint(:new)
288
- pc2.name = "#{supertype.name}MayBeA#{subtype.name}"
289
- pc2.vocabulary = @vocabulary
290
- pc2.role_sequence = p2rs
291
- pc2.is_mandatory = false
292
- pc2.min_frequency = 0
293
- pc2.max_frequency = 1
294
- pc2.is_preferred_identifier = inheritance_fact.provides_identification
295
- =end
296
- }
265
+ debug :orm, "Reading sub-types" do
266
+ @x_subtypes.each{|x|
267
+ id = x['id']
268
+ name = (x['Name'] || x['_Name'] || '').gsub(/\s+/,' ').gsub(/-/,'_').strip
269
+ name = nil if name.size == 0
270
+ debug :orm, "FactType #{name || id}"
271
+
272
+ x_subtype_role = x.xpath('orm:FactRoles/orm:SubtypeMetaRole')[0]
273
+ subtype_role_id = x_subtype_role['id']
274
+ subtype_id = x_subtype_role.xpath('orm:RolePlayer')[0]['ref']
275
+ subtype = @by_id[subtype_id]
276
+ # REVISIT: Provide a way in the metamodel of handling Partition, (and mapping choices that vary for each supertype?)
277
+
278
+ x_supertype_role = x.xpath('orm:FactRoles/orm:SupertypeMetaRole')[0]
279
+ supertype_role_id = x_supertype_role['id']
280
+ supertype_id = x_supertype_role.xpath('orm:RolePlayer')[0]['ref']
281
+ supertype = @by_id[supertype_id]
282
+
283
+ throw "For Subtype fact #{name}, the supertype #{supertype_id} was not found" if !supertype
284
+ throw "For Subtype fact #{name}, the subtype #{subtype_id} was not found" if !subtype
285
+ debug :orm, "#{subtype.name} is a subtype of #{supertype.name}"
286
+
287
+ inheritance_fact = @constellation.TypeInheritance(subtype, supertype)
288
+ inheritance_fact.fact_type_id = :new
289
+ if x["IsPrimary"] == "true" or # Old way
290
+ x["PreferredIdentificationPath"] == "true" # Newer
291
+ debug :orm, "#{supertype.name} is primary supertype of #{subtype.name}"
292
+ inheritance_fact.provides_identification = true
293
+ end
294
+ mapping = @x_mappings.detect{ |m| m['ref'] == id }
295
+ mapping_choice = mapping ? mapping.parent['AbsorptionChoice'] : 'Absorbed'
296
+ inheritance_fact.assimilation = mapping_choice.downcase.sub(/partition/, 'partitioned') if mapping_choice != 'Absorbed'
297
+ facts << @by_id[id] = inheritance_fact
298
+
299
+ # Create the new Roles so we can find constraints on them:
300
+ subtype_role = @by_id[subtype_role_id] = @constellation.Role(inheritance_fact, 0, :concept => subtype)
301
+ supertype_role = @by_id[supertype_role_id] = @constellation.Role(inheritance_fact, 1, :concept => supertype)
302
+
303
+ # Create readings, so constraints can be verbalised for example:
304
+ rs = @constellation.RoleSequence(:new)
305
+ @constellation.RoleRef(rs, 0, :role => subtype_role)
306
+ @constellation.RoleRef(rs, 1, :role => supertype_role)
307
+ @constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :text => "{0} is a kind of {1}")
308
+ @constellation.Reading(inheritance_fact, 1, :role_sequence => rs, :text => "{0} is a subtype of {1}")
309
+
310
+ rs2 = @constellation.RoleSequence(:new)
311
+ @constellation.RoleRef(rs2, 0, :role => supertype_role)
312
+ @constellation.RoleRef(rs2, 1, :role => subtype_role)
313
+ n = 'aeiouh'.include?(subtype_role.concept.name.downcase[0]) ? 1 : 0
314
+ @constellation.Reading(inheritance_fact, 2+n, :role_sequence => rs2, :text => "{0} is a {1}")
315
+ @constellation.Reading(inheritance_fact, 3-n, :role_sequence => rs2, :text => "{0} is an {1}")
316
+ }
317
+ end
297
318
  end
298
319
 
299
320
  def read_nested_types
300
321
  # Process NestedTypes, but ignore ones having a NestedPredicate with IsImplied="true"
301
322
  # We'll ignore the fact roles (and constraints) that implied objectifications have.
302
323
  # This happens for all ternaries and higher order facts
303
- nested_types = []
324
+ @nested_types = []
304
325
  x_nested_types = @x_model.xpath("orm:Objects/orm:ObjectifiedType")
305
- x_nested_types.each{|x|
306
- id = x['id']
307
- name = x['Name'] || ""
308
- name.gsub!(/\s/,'')
309
- name = nil if name.size == 0
326
+ debug :orm, "Reading objectified types" do
327
+ x_nested_types.each{|x|
328
+ id = x['id']
329
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
330
+ name = nil if name.size == 0
310
331
 
311
- x_fact_type = x.xpath('orm:NestedPredicate')[0]
312
- is_implied = x_fact_type['IsImplied'] == "true"
313
-
314
- fact_id = x_fact_type['ref']
315
- fact_type = @by_id[fact_id]
316
- throw "Nested fact #{fact_id} not found" if !fact_type
317
-
318
- #if is_implied
319
- # puts "Implied type #{name} (#{id}) nests #{fact_type ? fact_type.fact_type_id : "unknown"}"
320
- # @by_id[id] = fact_type
321
- #else
322
- begin
323
- #puts "NestedType #{name} is #{id}, nests #{fact_type.fact_type_id}"
324
- nested_types <<
325
- @by_id[id] =
326
- nested_type = @constellation.EntityType(@vocabulary, name)
327
- nested_type.fact_type = fact_type
328
- end
329
- }
332
+ x_fact_type = x.xpath('orm:NestedPredicate')[0]
333
+ is_implied = x_fact_type['IsImplied'] == "true"
334
+
335
+ fact_id = x_fact_type['ref']
336
+ fact_type = @by_id[fact_id]
337
+ throw "Nested fact #{fact_id} not found" if !fact_type
338
+
339
+ #if is_implied
340
+ # puts "Implied type #{name} (#{id}) nests #{fact_type ? fact_type.fact_type_id : "unknown"}"
341
+ # @by_id[id] = fact_type
342
+ #else
343
+ begin
344
+ debug :orm, "NestedType #{name} is #{id}, nests #{fact_type.fact_type_id}"
345
+ @nested_types <<
346
+ @by_id[id] =
347
+ nested_type = @constellation.EntityType(@vocabulary, name)
348
+ nested_type.fact_type = fact_type
349
+ end
350
+ }
351
+ end
330
352
  end
331
353
 
332
- def read_roles
333
- @x_facts.each{|x|
334
- id = x['id']
335
- fact_type = @by_id[id]
336
- fact_name = x['Name'] || x['_Name'] || ''
337
- fact_name.gsub!(/\s/,'')
338
- fact_name = nil if fact_name == ''
339
-
340
- x_fact_roles = x.xpath('orm:FactRoles/*')
341
- x_reading_orders = x.xpath('orm:ReadingOrders/*')
342
-
343
- # Deal with FactRoles (Roles):
344
- x_fact_roles.each{|x|
345
- name = x['Name'] || ""
346
- name.gsub!(/\s/,'')
347
- name = nil if name.size == 0
354
+ def complete_nested_types
355
+ @nested_types.each do |nested_type|
356
+ # Create the phantom roles here. These will be used later when we create objectification joins,
357
+ # but for now there's nothing we import from NORMA which requires objectification joins.
358
+ # Consequently there's no need to index them against NORMA's phantom roles.
359
+ nested_type.create_implicit_fact_types
360
+ end
361
+ end
348
362
 
349
- # _IsMandatory = x['_IsMandatory']
350
- # _Multiplicity = x['_Multiplicity]
363
+ def read_roles
364
+ debug :orm, "Reading roles and readings" do
365
+ @x_facts.each{|x|
351
366
  id = x['id']
352
- ref = x.xpath('orm:RolePlayer')[0]['ref']
353
-
354
- # Find the concept that plays the role:
355
- concept = @by_id[ref]
356
- throw "RolePlayer for '#{name}' #{ref} was not found" if !concept
357
-
358
- # Skip implicit roles added by NORMA to make unaries into binaries.
359
- # This would make constraints over the deleted roles impossible,
360
- # so as a SPECIAL CASE we index the unary role by the id of the
361
- # implicit role. That means care is needed when handling unary FTs.
362
- if (ox = @x_by_id[ref]) && ox['IsImplicitBooleanValue']
363
- x_other_role = x.parent.xpath('orm:Role').reject{|x_role|
364
- x_role == x
365
- }[0]
366
- other_role_id = x_other_role["id"]
367
- other_role = @by_id[other_role_id]
368
- # puts "Indexing unary FT role #{other_role_id} by implicit boolean role #{id}"
369
- @by_id[id] = other_role
370
-
371
- # The role name of the ignored role is the one that applies:
372
- role_name = x['Name']
373
- other_role.role_name = role_name if role_name && role_name != ''
374
-
375
- concept.deny # Delete our object for the implicit boolean ValueType
376
- @by_id.delete(ref) # and de-index it from our list
377
- next
378
- end
379
-
380
- #puts "#{@vocabulary}, Name=#{x['Name']}, concept=#{concept}"
381
- throw "Role is played by #{concept.class} not Concept" if !(@constellation.vocabulary.concept(:Concept) === concept)
367
+ fact_type = @by_id[id]
368
+ fact_name = x['Name'] || x['_Name'] || ''
369
+ #fact_name.gsub!(/\s/,'')
370
+ fact_name = nil if fact_name == ''
371
+
372
+ x_fact_roles = x.xpath('orm:FactRoles/*')
373
+ x_reading_orders = x.xpath('orm:ReadingOrders/*')
374
+
375
+ # Deal with FactRoles (Roles):
376
+ debug :orm, "Reading fact roles" do
377
+ x_fact_roles.each{|x|
378
+ name = (x['Name'] || "").gsub(/\s+/,' ').gsub(/-/,'_').strip
379
+ name = nil if name.size == 0
380
+
381
+ # _IsMandatory = x['_IsMandatory']
382
+ # _Multiplicity = x['_Multiplicity]
383
+ id = x['id']
384
+ rp = x.xpath('orm:RolePlayer')[0]
385
+ raise "Invalid ORM file; fact has missing player (RolePlayer id=#{id})" unless rp
386
+ ref = rp['ref']
387
+
388
+ # Find the concept that plays the role:
389
+ concept = @by_id[ref]
390
+ throw "RolePlayer for '#{name}' #{ref} was not found" if !concept
391
+
392
+ # Skip implicit roles added by NORMA to make unaries into binaries.
393
+ # This would make constraints over the deleted roles impossible,
394
+ # so as a SPECIAL CASE we index the unary role by the id of the
395
+ # implicit role. That means care is needed when handling unary FTs.
396
+ if (ox = @x_by_id[ref]) && ox['IsImplicitBooleanValue']
397
+ x_other_role = x.parent.xpath('orm:Role').reject{|x_role|
398
+ x_role == x
399
+ }[0]
400
+ other_role_id = x_other_role["id"]
401
+ other_role = @by_id[other_role_id]
402
+ debug :orm, "Indexing unary FT role #{other_role_id} by implicit boolean role #{id}"
403
+ @by_id[id] = other_role
404
+
405
+ # The role name of the ignored role is the one that applies:
406
+ role_name = x['Name']
407
+ other_role.role_name = role_name if role_name && role_name != ''
408
+
409
+ concept.retract # Delete our object for the implicit boolean ValueType
410
+ @by_id.delete(ref) # and de-index it from our list
411
+ next
412
+ end
413
+
414
+ debug :orm, "#{@vocabulary.name}, RoleName=#{x['Name'].inspect} played by concept=#{concept.name}"
415
+ throw "Role is played by #{concept.class} not Concept" if !(@constellation.vocabulary.concept(:Concept) === concept)
416
+
417
+ debug :orm, "Creating role #{name} nr#{fact_type.all_role.size} of #{fact_type.fact_type_id} played by #{concept.name}"
418
+
419
+ role = @by_id[id] = @constellation.Role(fact_type, fact_type.all_role.size, :concept => concept)
420
+ role.role_name = name if name
421
+ debug :orm, "Fact #{fact_name} (id #{fact_type.fact_type_id.object_id}) role #{x['Name']} is played by #{concept.name}, role is #{role.object_id}"
422
+
423
+ x_vr = x.xpath("orm:ValueRestriction/orm:RoleValueConstraint")
424
+ x_vr.each{|vr|
425
+ x_ranges = vr.xpath("orm:ValueRanges/orm:ValueRange")
426
+ next if x_ranges.size == 0
427
+ role.role_value_constraint = @by_id[vr['id']] = @constellation.ValueConstraint(:new)
428
+ x_ranges.each{|x_range|
429
+ v_range = value_range(x_range)
430
+ ar = @constellation.AllowedRange(role.role_value_constraint, v_range)
431
+ }
432
+ }
382
433
 
383
- name = x['Name'] || ''
384
- name.gsub!(/\s/,'')
385
- name = nil if name.size == 0
386
- #puts "Creating role #{name} nr#{fact_type.all_role.size} of #{fact_type.fact_type_id} played by #{concept.name}"
387
-
388
- role = @by_id[id] = @constellation.Role(fact_type, fact_type.all_role.size, :concept => concept)
389
- role.role_name = name if name
390
- # puts "Fact #{fact_name} (id #{fact_type.fact_type_id.object_id}) role #{x['Name']} is played by #{concept.name}, role is #{role.object_id}"
391
-
392
- x_vr = x.xpath("orm:ValueRestriction")
393
- x_vr.each{|vr|
394
- x_ranges = vr.xpath("orm:RoleValueConstraint/orm:ValueRanges/orm:ValueRange")
395
- next if x_ranges.size == 0
396
- role.role_value_restriction = @constellation.ValueRestriction(:new)
397
- x_ranges.each{|x_range|
398
- v_range = value_range(x_range)
399
- ar = @constellation.AllowedRange(role.role_value_restriction, v_range)
434
+ debug :orm, "Adding Role #{role.role_name || role.concept.name} to #{fact_type.describe}"
435
+ #fact_type.add_role(role)
436
+ debug :orm, "Role #{role} is #{id}"
400
437
  }
401
- }
402
-
403
- # puts "Adding Role #{role.name} to #{fact_type.name}"
404
- #fact_type.add_role(role)
405
- # puts "\tRole #{role} is #{id}"
406
- }
438
+ end
407
439
 
408
- # Deal with Readings:
409
- x_reading_orders.each{|x|
410
- x_role_sequence = x.xpath('orm:RoleSequence/*')
411
- x_readings = x.xpath('orm:Readings/orm:Reading/orm:Data')
440
+ # Deal with Readings:
441
+ debug :orm, "Reading fact readings" do
442
+ x_reading_orders.each{|x|
443
+ x_role_sequence = x.xpath('orm:RoleSequence/*')
444
+ x_readings = x.xpath('orm:Readings/orm:Reading/orm:Data')
412
445
 
413
- # Build an array of the Roles needed:
414
- role_array = x_role_sequence.map{|x| @by_id[x['ref']] }
446
+ # Build an array of the Roles needed:
447
+ role_array = x_role_sequence.map{|x| @by_id[x['ref']] }
415
448
 
416
- # puts "Reading #{x_readings.map(&:text).inspect}"
417
- role_sequence = get_role_sequence(role_array)
449
+ debug :orm, "Reading #{x_readings.map(&:text).inspect}"
450
+ role_sequence = get_role_sequence(role_array)
418
451
 
419
- #role_sequence.all_role_ref.each_with_index{|rr, i|
420
- # # REVISIT: rr.leading_adjective = ...; Add adjectives here
421
- # }
452
+ #role_sequence.all_role_ref.each_with_index{|rr, i|
453
+ # # REVISIT: rr.leading_adjective = ...; Add adjectives here
454
+ # }
422
455
 
423
- x_readings.each_with_index{|x, i|
424
- reading = @constellation.Reading(fact_type, fact_type.all_reading.size)
425
- reading.role_sequence = role_sequence
426
- # REVISIT: The downcase here only needs to be the initial letter of each word, but be safe:
427
- reading.text = extract_adjectives(x.text, role_sequence).downcase
428
- }
456
+ x_readings.each_with_index{|x, i|
457
+ reading = @constellation.Reading(fact_type, fact_type.all_reading.size)
458
+ reading.role_sequence = role_sequence
459
+ # REVISIT: The downcase here only needs to be the initial letter of each word, but be safe:
460
+ reading.text = extract_adjectives(x.text, role_sequence)
461
+ }
462
+ }
463
+ end
429
464
  }
430
- }
431
- # @vocabulary.fact_types.each{|ft| puts ft }
465
+ end
432
466
  end
433
467
 
434
468
  def extract_adjectives(text, role_sequence)
@@ -437,15 +471,17 @@ module ActiveFacts
437
471
  role_ref = all_role_refs[i]
438
472
  role = role_ref.role
439
473
 
440
- word = '\b([A-Za-z][A-Za-z0-9_]*)\b'
441
- leading_adjectives_re = "(?:#{word}- *(?:#{word} +)?)"
442
- trailing_adjectives_re = "(?: +(?:#{word} +) *-#{word}?)"
474
+ word = '\b[A-Za-z_][A-Za-z0-9_]+\b'
475
+ leading_adjectives_re = "#{word}-(?: +#{word})*"
476
+ trailing_adjectives_re = "(?:#{word} +)*-#{word}"
443
477
  role_with_adjectives_re =
444
- %r| ?#{leading_adjectives_re}?\{#{i}\}#{trailing_adjectives_re}? ?|
478
+ %r| ?(#{leading_adjectives_re})? *\{#{i}\} *(#{trailing_adjectives_re})? ?|
445
479
 
446
480
  text.gsub!(role_with_adjectives_re) {
447
- la = [[$1]*"", [$2]*""]*" ".gsub(/\s+/, ' ').sub(/\s+\Z/,'').strip
448
- ta = [[$1]*"", [$2]*""]*" ".gsub(/\s+/, ' ').sub(/\A\s+/,'').strip
481
+ # REVISIT: Don't want to strip all spaces here any more:
482
+ #puts "text=#{text.inspect}, la=#{$1.inspect}, ta=#{$2.inspect}" if $1 || $2
483
+ la = ($1||'').gsub(/\s+/,' ').sub(/-/,'').strip
484
+ ta = ($2||'').gsub(/\s+/,' ').sub(/-/,'').strip
449
485
  #puts "Setting leading adj #{la.inspect} from #{text.inspect} for #{role_ref.role.concept.name}" if la != ""
450
486
  # REVISIT: Dunno what's up here, but removing the "if" test makes this chuck exceptions:
451
487
  role_ref.leading_adjective = la if la != ""
@@ -456,8 +492,34 @@ module ActiveFacts
456
492
  " {#{i}} "
457
493
  }
458
494
  }
459
- text.sub!(/\A /, '')
460
- text.sub!(/ \Z/, '')
495
+ text.sub!(/\s\s*/, ' ') # Compress extra spaces
496
+ text.strip!
497
+ text.downcase! # Check for reserved words and object type names *after* downcasing
498
+ elided = ''
499
+ text.gsub!(/( |-?\b[A-Za-z_][A-Za-z0-9_]*\b-?|\{\d\})|./) do |w|
500
+ case w
501
+ when /[A-Za-z]/
502
+ if RESERVED_WORDS.include?(w)
503
+ $stderr.puts "Masking reserved word '#{w}' in reading '#{text}'"
504
+ next "_#{w}"
505
+ elsif @constellation.Concept[[[@vocabulary.name], w]]
506
+ $stderr.puts "Masking object type name '#{w}' in reading '#{text}'"
507
+ next "_#{w}"
508
+ elsif all_role_refs.detect{|rr| rr.role.role_name == w}
509
+ $stderr.puts "Masking role name '#{w}' in reading '#{text}'"
510
+ next "_#{w}"
511
+ end
512
+ next w
513
+ when /\{\d\}/
514
+ next w
515
+ when / /
516
+ next w
517
+ else
518
+ elided << w
519
+ next ''
520
+ end
521
+ end
522
+ $stderr.puts "Elided illegal characters '#{elided}' from reading" unless elided.empty?
461
523
  text
462
524
  end
463
525
 
@@ -475,15 +537,16 @@ module ActiveFacts
475
537
 
476
538
  # Make a new RoleSequence:
477
539
  role_sequence = @constellation.RoleSequence(:new) unless role_sequence
478
- role_array.each_with_index{|r, i|
540
+ role_array.each_with_index do |r, i|
479
541
  role_ref = @constellation.RoleRef(role_sequence, i)
480
542
  role_ref.role = r
481
- }
543
+ end
544
+
482
545
  role_sequence
483
546
  end
484
547
 
485
548
  def map_roles(x_roles, why = nil)
486
- role_array = x_roles.map{|x|
549
+ role_array = x_roles.map do |x|
487
550
  id = x['ref']
488
551
  role = @by_id[id]
489
552
  if (why && !role)
@@ -514,8 +577,10 @@ module ActiveFacts
514
577
  end
515
578
  end
516
579
  role
517
- }
518
- role_array.include?(nil) ? nil : get_role_sequence(role_array)
580
+ end
581
+ return nil if role_array.include?(nil)
582
+
583
+ get_role_sequence(role_array)
519
584
  end
520
585
 
521
586
  def read_constraints
@@ -535,378 +600,747 @@ module ActiveFacts
535
600
  x_mandatory_constraints = @x_model.xpath("orm:Constraints/orm:MandatoryConstraint")
536
601
  @mandatory_constraints_by_rs = {}
537
602
  @mandatory_constraint_rs_by_id = {}
538
- x_mandatory_constraints.each{|x|
539
- name = x["Name"] || ''
540
- name.gsub!(/\s/,'')
541
- name = nil if name.size == 0
542
-
543
- # As of Feb 2008, all NORMA ValueTypes have an implied mandatory constraint.
544
- if x.xpath("orm:ImpliedByObjectType").size > 0
545
- # $stderr.puts "Skipping ImpliedMandatoryConstraint #{name} over #{roles}"
546
- next
547
- end
603
+ debug :orm, "Scanning mandatory constraints" do
604
+ x_mandatory_constraints.each{|x|
605
+ name = x["Name"] || ''
606
+ name = nil if name.size == 0
548
607
 
549
- x_roles = x.xpath("orm:RoleSequence/orm:Role")
550
- roles = map_roles(x_roles, "mandatory constraint #{name}")
551
- next if !roles
608
+ # As of Feb 2008, all NORMA ValueTypes have an implied mandatory constraint.
609
+ next if x.xpath("orm:ImpliedByObjectType").size > 0
552
610
 
553
- # If X-OR mandatory, the Exclusion is accessed by:
554
- # x_exclusion = (ex = x.xpath("orm:ExclusiveOrExclusionConstraint")[0]) &&
555
- # @x_by_id[ex['ref']]
556
- # puts "Mandatory #{name}(#{roles}) is paired with exclusive #{x_exclusion['Name']}" if x_exclusion
611
+ x_roles = x.xpath("orm:RoleSequence/orm:Role")
612
+ role_sequence = map_roles(x_roles, "mandatory constraint #{name}")
613
+ next if !role_sequence
557
614
 
558
- @mandatory_constraints_by_rs[roles] = x
559
- @mandatory_constraint_rs_by_id[x['id']] = roles
560
- }
615
+ debug :orm, "New MC #{x['Name']} over #{role_sequence.describe}"
616
+ @mandatory_constraints_by_rs[role_sequence] = x
617
+ @mandatory_constraint_rs_by_id[x['id']] = role_sequence
618
+ }
619
+ end
561
620
  end
562
621
 
622
+ # Mandatory constraints that didn't get merged with an exclusion constraint or a uniqueness constraint are simple mandatories
563
623
  def read_residual_mandatory_constraints
564
- @mandatory_constraints_by_rs.each { |roles, x|
565
- # Create a simply-mandatory PresenceConstraint for each mandatory constraint
566
- name = x["Name"] || ''
567
- name.gsub!(/\s/,'')
568
- name = nil if name.size == 0
569
- #puts "Residual Mandatory #{name}: #{roles.to_s}"
570
-
571
- pc = @constellation.PresenceConstraint(:new)
572
- pc.vocabulary = @vocabulary
573
- pc.name = name
574
- pc.role_sequence = roles
575
- pc.is_mandatory = true
576
- pc.min_frequency = 1
577
- pc.max_frequency = nil
578
- pc.is_preferred_identifier = false
579
-
580
- (@constraints_by_rs[roles] ||= []) << pc
581
- }
624
+ debug :orm, "Processing non-absorbed mandatory constraints" do
625
+ @mandatory_constraints_by_rs.each { |role_sequence, x|
626
+ id = x['id']
627
+ # Create a simply-mandatory PresenceConstraint for each mandatory constraint
628
+ name = x["Name"] || ''
629
+ name = nil if name.size == 0
630
+ #puts "Residual Mandatory #{name}: #{role_sequence.to_s}"
631
+
632
+ if (players = role_sequence.all_role_ref.map{|rr| rr.role.concept}).uniq.size > 1
633
+ join_over, = *ActiveFacts::Metamodel.join_roles_over(role_sequence.all_role_ref.map{|rr| rr.role}, :proximate)
634
+ raise "Mandatory join constraint #{name} has incompatible players #{players.map{|o| o.name}.inspect}" unless join_over
635
+ if players.detect{|p| p != join_over}
636
+ debug :join, "subtyping join simple mandatory constraint #{name} over #{join_over.name}"
637
+ players.each_with_index do |player, i|
638
+ next if player != join_over
639
+ # REVISIT: We don't need to make a subtyping join here (from join_over to player)
640
+ end
641
+ end
642
+ end
643
+
644
+ pc = @constellation.PresenceConstraint(:new)
645
+ pc.vocabulary = @vocabulary
646
+ pc.name = name
647
+ pc.role_sequence = role_sequence
648
+ pc.is_mandatory = true
649
+ pc.min_frequency = 1
650
+ pc.max_frequency = nil
651
+ pc.is_preferred_identifier = false
652
+
653
+ (@constraints_by_rs[role_sequence] ||= []) << pc
654
+ @by_id[id] = pc
655
+ }
656
+ end
582
657
  end
583
658
 
584
659
  def read_uniqueness_constraints
585
660
  x_uniqueness_constraints = @x_model.xpath("orm:Constraints/orm:UniquenessConstraint")
586
- x_uniqueness_constraints.each{|x|
587
- name = x["Name"] || ''
588
- name.gsub!(/\s/,'')
589
- name = nil if name.size == 0
590
- id = x["id"]
591
- x_pi = x.xpath("orm:PreferredIdentifierFor")[0]
592
- pi = x_pi ? @by_id[eref = x_pi['ref']] : nil
593
-
594
- # Skip uniqueness constraints on implied concepts
595
- if x_pi && !pi
596
- puts "Skipping uniqueness constraint #{name}, entity not found"
597
- next
661
+ debug :orm, "Reading uniqueness constraints" do
662
+ x_uniqueness_constraints.each{|x|
663
+ name = x["Name"] || ''
664
+ name = nil if name.size == 0
665
+ uc_id = x["id"]
666
+ x_pi = x.xpath("orm:PreferredIdentifierFor")[0]
667
+ pi = x_pi ? @by_id[eref = x_pi['ref']] : nil
668
+
669
+ # Skip uniqueness constraints on implied concepts
670
+ next if x_pi && !pi
671
+
672
+ # Get the RoleSequence:
673
+ x_roles = x.xpath("orm:RoleSequence/orm:Role")
674
+ next if x_roles.size == 0
675
+ role_sequence = map_roles(x_roles, "uniqueness constraint #{name}")
676
+ next if !role_sequence
677
+ #puts "uc: #{role_sequence.all_role_ref.map{|rr|rr.role.fact_type.default_reading}*', '}"
678
+
679
+ # Check for a join
680
+ if (fact_types = role_sequence.all_role_ref.map{|rr| rr.role.fact_type}).uniq.size > 1
681
+ join_over, = *ActiveFacts::Metamodel.join_roles_over(role_sequence.all_role_ref.map{|rr| rr.role}, :counterpart)
682
+
683
+ players = role_sequence.all_role_ref.map{|rr| rr.role.concept.name}.uniq
684
+ raise "Uniqueness join constraint #{name} has incompatible players #{players.inspect}" unless join_over
685
+ subtyping = players.size > 1 ? 'subtyping ' : ''
686
+ # REVISIT: Create the Join, the JoinNode for join_over, and steps from each role_ref to join_over
687
+ debug :join, "#{subtyping}join uniqueness constraint over #{join_over.name} in #{fact_types.map(&:default_reading)*', '}"
688
+ end
689
+
690
+ # There is an implicit uniqueness constraint when any object plays a unary. Skip it.
691
+ if (x_roles.size == 1 &&
692
+ (id = x_roles[0]['ref']) &&
693
+ (x_role = @x_by_id[id]) &&
694
+ (nodes = x_role.parent.elements).size == 2 &&
695
+ (sibling = nodes[1]) &&
696
+ (ib_id = sibling.elements[0]['ref']) &&
697
+ (ib = @x_by_id[ib_id]) &&
698
+ ib['IsImplicitBooleanValue'])
699
+ unary_identifier = true
700
+ end
701
+
702
+ mc_id = nil
703
+ if (mc = @mandatory_constraints_by_rs[role_sequence])
704
+ # Remove absorbed mandatory constraints, leaving residual ones.
705
+ debug :orm, "Absorbing MC #{mc['Name']} over #{role_sequence.describe}"
706
+ @mandatory_constraints_by_rs.delete(role_sequence)
707
+ mc_id = mc['id']
708
+ @mandatory_constraint_rs_by_id.delete(mc['id'])
709
+ else
710
+ debug :orm, "No MC to absorb over #{role_sequence.describe}"
711
+ end
712
+
713
+ # A TypeInheritance fact type has a uniqueness constraint on each role.
714
+ # If this UC is on the supertype and identifies the subtype, it's preferred:
715
+ is_supertype_constraint =
716
+ (rr = role_sequence.all_role_ref.single) &&
717
+ (role = rr.role) &&
718
+ (fact_type = role.fact_type) &&
719
+ fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
720
+ role.concept == fact_type.supertype &&
721
+ fact_type.provides_identification
722
+
723
+ pc = @constellation.PresenceConstraint(:new)
724
+ pc.vocabulary = @vocabulary
725
+ pc.name = name
726
+ pc.role_sequence = role_sequence
727
+ pc.is_mandatory = true if mc
728
+ pc.min_frequency = mc ? 1 : 0
729
+ pc.max_frequency = 1
730
+ pc.is_preferred_identifier = true if pi || unary_identifier || is_supertype_constraint
731
+ debug :orm, "#{name} covers #{role_sequence.describe} has min=#{pc.min_frequency}, max=1, preferred=#{pc.is_preferred_identifier.inspect}"
732
+
733
+ debug :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 }
734
+
735
+ (@constraints_by_rs[role_sequence] ||= []) << pc
736
+ @by_id[uc_id] = pc
737
+ @by_id[mc_id] = pc if mc_id
738
+ }
739
+ end
740
+ end
741
+
742
+ # Equality and subset joins involve two or more role sequences,
743
+ # and the respective roles from each sequence must be compatible,
744
+ # Compatibility might involve subtyping joins but not objectification joins
745
+ # to the respective end-point (constrained object type).
746
+ # Also, all roles in each sequence constitute a join over a single
747
+ # object type, which might involve subtyping or objectification joins.
748
+ #
749
+ def make_joins(constraint_type, name, role_sequences)
750
+ # Get the object types constrained for each position in the role sequences.
751
+ # Supertyping joins may be needed to reach them.
752
+ end_points = [] # An array of the common supertype for matching role_refs across the sequences
753
+ end_joins = [] # An array of booleans indicating whether any role_sequence requires a subtyping join
754
+ role_sequences[0].all_role_ref.size.times do |i|
755
+ role_refs = role_sequences.map{|rs| rs.all_role_ref.detect{|rr| rr.ordinal == i}}
756
+ next if (fact_types = role_refs.map{|rr| rr.role.fact_type}).uniq.size == 1
757
+ if (players = role_refs.map{|rr| rr.role.concept}).uniq.size == 1
758
+ end_point = players[0]
759
+ end_joins[i] = false
760
+ else
761
+ # Can the players be joined using a subtyping join?
762
+ common_supertypes = players[1..-1].
763
+ inject(players[0].supertypes_transitive) do |remaining, player|
764
+ remaining & player.supertypes_transitive
765
+ end
766
+ end_point = common_supertypes[0]
767
+
768
+ raise "constrained roles of #{constraint_type} constraint #{name} are incompatible (#{names*', '})" if common_supertypes.size == 0
769
+ end_joins[i] = true
770
+ end
771
+ end_points[i] = end_point
772
+ end
773
+
774
+ # For each role_sequence, find the object type over which the join is implied (nil if no join)
775
+ sequence_join_over = []
776
+ if role_sequences[0].all_role_ref.size > 1 # There are joins within each sequence.
777
+ sequence_join_over = role_sequences.map do |rs|
778
+ join_over, joined_roles = *ActiveFacts::Metamodel.join_roles_over(rs.all_role_ref.map{|rr| rr.role})
779
+ join_over
598
780
  end
781
+ end
599
782
 
600
- # A uniqueness constraint on a fact having an implied objectification isn't preferred:
601
- # if pi &&
602
- # (x_pi_for = @x_by_id[eref]) &&
603
- # (np = x_pi_for.xpath('orm:NestedPredicate')[0]) &&
604
- # np['IsImplied']
605
- # pi = nil
606
- # end
783
+ # If there are no joins, we can drop out here.
784
+ if sequence_join_over.compact.empty? && !end_joins.detect{|e| true}
785
+ return
786
+ end
787
+
788
+ debug :join, "#{constraint_type} join constraint #{name} over #{role_sequences.map{|rs|rs.describe}*', '}"
607
789
 
608
- # Get the RoleSequence:
609
- x_roles = x.xpath("orm:RoleSequence/orm:Role")
610
- next if x_roles.size == 0
611
- roles = map_roles(x_roles, "uniqueness constraint #{name}")
612
- next if !roles
613
-
614
- # There is an implicit uniqueness constraint when any object plays a unary. Skip it.
615
- if (x_roles.size == 1 &&
616
- (id = x_roles[0]['ref']) &&
617
- (x_role = @x_by_id[id]) &&
618
- (nodes = x_role.parent.elements).size == 2 &&
619
- (sibling = nodes[1]) &&
620
- # x_role.parent.children.size == 2 &&
621
- # (sibling = x_role.parent.children[1]) &&
622
- (ib_id = sibling.elements[0]['ref']) &&
623
- (ib = @x_by_id[ib_id]) &&
624
- ib['IsImplicitBooleanValue'])
625
- unary_identifier = true
790
+ join = nil
791
+ debug :join, "#{constraint_type} join constraint #{name} constrains #{
792
+ end_points.zip(end_joins).map{|(p,j)| p.name+(j ? ' & subtypes':'')}*', '
793
+ }#{
794
+ if role_sequences[0].all_role_ref.size > 1
795
+ ", joined over #{sequence_join_over.map{|o| o ? o.name : '(none)'}*', '}"
796
+ else
797
+ ''
626
798
  end
799
+ }" do
800
+
801
+ end
802
+ return # REVISIT: Disabled until we get the join verbaliser working.
803
+ while false
804
+
805
+ # There may be one join per role sequence:
806
+ role_sequences.zip(sequence_join_over).map do |role_sequence, join_over|
807
+ # Skip if there's no join here (sequence join nor end-point subset join)
808
+ role_refs = role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}
809
+ next unless join_over or role_refs.detect{|rr| rr.role.concept != end_points[rr.ordinal]}
810
+
811
+ replacement_rs = @constellation.RoleSequence(:new)
812
+ join = @constellation.Join(:new)
813
+ join_node = nil
814
+ role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}.each_with_index do |role_ref, i|
815
+ end_point = end_points[i]
816
+ debug :join, "Join Node #{join.all_join_node.size} is for #{end_point.name}"
817
+ end_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => end_point)
818
+ role_node = end_node
819
+
820
+ if role_ref.role.concept != end_point
821
+ subtype = role_ref.role.concept
822
+ debug :join, "Making subtyping join step #{join.all_join_node.size} from #{subtype.name} to #{end_point.name}" do
823
+ role_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => role_ref.role.concept)
824
+ ti = role_ref.role.concept.all_type_inheritance_as_subtype.detect{|ti| ti.supertype == end_point}
825
+ raise "REVISIT: Can't yet handle multiple subtyping levels in subtyping join" unless ti
826
+
827
+ # Make a new role sequence over the subtyping fact type:
828
+ rs = @constellation.RoleSequence(:new)
829
+ @constellation.RoleRef(rs, 0, :role => ti.all_role.detect{|r| r.concept == role_ref.role.concept}, :join_node => role_node)
830
+ @constellation.RoleRef(rs, 1, :role => ti.all_role.detect{|r| r.concept == end_point}, :join_node => end_node)
831
+
832
+ js = @constellation.JoinStep(role_node, end_node, :fact_type => ti)
833
+ debug :join, "New subtyping join step #{js.describe}"
834
+ end
835
+ end
627
836
 
628
- if (mc = @mandatory_constraints_by_rs[roles])
629
- # Remove absorbed mandatory constraints, leaving residual ones.
630
- # puts "Absorbing MC #{mc['Name']}"
631
- @mandatory_constraints_by_rs.delete(roles)
632
- @mandatory_constraint_rs_by_id.delete(mc['id'])
837
+ @constellation.RoleRef(replacement_rs, 0, :role => role_ref.role, :join_node => end_node)
838
+
839
+ # REVISIT: Something here needs to handle unaries, which have a join step over two copies of the same JoinNode
840
+
841
+ if join_over
842
+ if !join_node
843
+ debug :join, "Join Node #{join.all_join_node.size} is over #{join_over.name}"
844
+ join_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => join_over)
845
+ end
846
+ debug :join, "Making join step from #{end_point.name} to #{join_over.name}" do
847
+ role = role_ref.role
848
+ # Detect an objectification step. Here, the role is in an objectified type, but the end_point isn't another role player of that fact type... or is there a better test?
849
+ if join_over == role.fact_type.entity_type
850
+ p join_over.name
851
+ p role.fact_type.default_reading
852
+ p end_point.name
853
+ raise "REVISIT: Incomplete"
854
+ elsif end_point == role.fact_type.entity_type
855
+ fact_type = role.implicit_fact_type
856
+ # REVISIT: Might these be reversed... sometimes?
857
+ end_role = fact_type.all_role.single
858
+ join_role = role
859
+ else
860
+ fact_type = role.fact_type
861
+ end_role = role
862
+ # This is unambiguous, since it's come from NORMA which only supports (unambiguous) implicit joins:
863
+ join_role = role.fact_type.all_role.detect{|r| r.concept == join_over}
864
+ # debugger unless join_role && end_role
865
+ end
866
+
867
+ rs = @constellation.RoleSequence(:new)
868
+ # Detect the fact type over which we're joining (may involve objectification)
869
+ @constellation.RoleRef(rs, 0, :role => end_role, :join_node => end_node)
870
+ @constellation.RoleRef(rs, 1, :role => join_role, :join_node => join_node)
871
+ js = @constellation.JoinStep(end_node, join_node, :fact_type => fact_type)
872
+ debug :join, "New join step #{js.describe}"
873
+ end
874
+ else
875
+ debug :join, "Need join step for non-join_over role #{end_point.name} #{role_ref.describe} in #{role_ref.role.fact_type.default_reading}"
876
+ if role_ref.role.fact_type.all_role.size > 1
877
+ raise unimplemented
878
+ else
879
+ js = @constellation.JoinStep(role_node, role_node, :fact_type => role_ref.role.fact_type)
880
+ end
881
+ end
882
+ end
883
+
884
+ # Thoroughly check that this is a valid join
885
+ join.validate
886
+
887
+ # Constrain the replacement role sequence, which has the attached join:
888
+ role_sequences[role_sequences.index(role_sequence)] = replacement_rs
633
889
  end
890
+ end
634
891
 
635
- # A UC that spans more than one Role of a fact will be a Preferred Id for the implied object
636
- #puts "Unique" + rs.to_s +
637
- # (pi ? " (preferred id for #{pi.name})" : "") +
638
- # (mc ? " (mandatory)" : "") if pi && !mc
639
-
640
- # A TypeInheritance fact type has a uniqueness constraint on each role.
641
- # If this UC is on the supertype and identifies the subtype, it's preferred:
642
- is_supertype_constraint =
643
- (rr = roles.all_role_ref.single) &&
644
- (role = rr.role) &&
645
- (fact_type = role.fact_type) &&
646
- fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
647
- role.concept == fact_type.supertype &&
648
- fact_type.provides_identification
649
-
650
- pc = @constellation.PresenceConstraint(:new)
651
- pc.vocabulary = @vocabulary
652
- pc.name = name
653
- pc.role_sequence = roles
654
- pc.is_mandatory = true if mc
655
- pc.min_frequency = mc ? 1 : 0
656
- pc.max_frequency = 1
657
- pc.is_preferred_identifier = true if pi || unary_identifier || is_supertype_constraint
658
- #puts "#{name} covers #{roles.describe} has min=#{pc.min_frequency}, max=1, preferred=#{pc.is_preferred_identifier.inspect}" if emit_special_debug
659
-
660
- #puts roles.all_role_ref.to_a[0].role.fact_type.describe + " is subject to " + pc.describe if roles.all_role_ref.all?{|r| r.role.fact_type.is_a? ActiveFacts::Metamodel::TypeInheritance }
661
-
662
- (@constraints_by_rs[roles] ||= []) << pc
663
- }
664
892
  end
665
893
 
666
894
  def read_exclusion_constraints
667
895
  x_exclusion_constraints = @x_model.xpath("orm:Constraints/orm:ExclusionConstraint")
668
- x_exclusion_constraints.each{|x|
669
- name = x["Name"] || ''
670
- name.gsub!(/\s/,'')
671
- name = nil if name.size == 0
672
- x_mandatory = (m = x.xpath("orm:ExclusiveOrMandatoryConstraint")[0]) &&
673
- @x_by_id[mc_id = m['ref']]
674
- role_sequences =
675
- x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
676
- x_role_refs = x_rs.xpath("orm:Role")
677
- map_roles(
678
- x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
679
- "exclusion constraint #{name}"
680
- )
681
- }
682
- if x_mandatory
683
- # Remove absorbed mandatory constraints, leaving residual ones.
684
- mc_rs = @mandatory_constraint_rs_by_id[mc_id]
685
- @mandatory_constraint_rs_by_id.delete(mc_id)
686
- @mandatory_constraints_by_rs.delete(mc_rs)
687
- end
896
+ debug :orm, "Reading exclusion constraints" do
897
+ x_exclusion_constraints.each{|x|
898
+ id = x['id']
899
+ name = x["Name"] || ''
900
+ name = nil if name.size == 0
901
+ x_mandatory = (m = x.xpath("orm:ExclusiveOrMandatoryConstraint")[0]) &&
902
+ @x_by_id[mc_id = m['ref']]
903
+ role_sequences =
904
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
905
+ x_role_refs = x_rs.xpath("orm:Role")
906
+ map_roles(
907
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
908
+ "exclusion constraint #{name}"
909
+ )
910
+ }
911
+ if x_mandatory
912
+ # Remove absorbed mandatory constraints, leaving residual ones.
913
+ mc_rs = @mandatory_constraint_rs_by_id[mc_id]
914
+ @mandatory_constraint_rs_by_id.delete(mc_id)
915
+ @mandatory_constraints_by_rs.delete(mc_rs)
916
+ end
688
917
 
689
- ec = @constellation.SetExclusionConstraint(:new)
690
- ec.vocabulary = @vocabulary
691
- ec.name = name
692
- # ec.enforcement =
693
- role_sequences.each_with_index do |rs, i|
694
- @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
695
- end
696
- ec.is_mandatory = true if x_mandatory
697
- }
918
+ make_joins('exclusion', name+(x_mandatory ? '/'+x_mandatory['Name'] : ''), role_sequences)
919
+
920
+ ec = @constellation.SetExclusionConstraint(:new)
921
+ ec.vocabulary = @vocabulary
922
+ ec.name = name
923
+ # ec.enforcement =
924
+ role_sequences.each_with_index do |rs, i|
925
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
926
+ end
927
+ ec.is_mandatory = true if x_mandatory
928
+ @by_id[id] = ec
929
+ @by_id[mc_id] = ec if mc_id
930
+ }
931
+ end
698
932
  end
699
933
 
700
934
  def read_equality_constraints
701
935
  x_equality_constraints = @x_model.xpath("orm:Constraints/orm:EqualityConstraint")
702
- x_equality_constraints.each{|x|
703
- name = x["Name"] || ''
704
- name.gsub!(/\s/,'')
705
- name = nil if name.size == 0
706
- role_sequences =
707
- x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
708
- x_role_refs = x_rs.xpath("orm:Role")
709
- map_roles(
710
- x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
711
- "equality constraint #{name}"
712
- )
713
- }
936
+ debug :orm, "Reading equality constraints" do
937
+ x_equality_constraints.each{|x|
938
+ id = x['id']
939
+ name = x["Name"] || ''
940
+ name = nil if name.size == 0
941
+ role_sequences =
942
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
943
+ x_role_refs = x_rs.xpath("orm:Role")
944
+ map_roles(
945
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
946
+ "equality constraint #{name}"
947
+ )
948
+ }
714
949
 
715
- ec = @constellation.SetEqualityConstraint(:new)
716
- ec.vocabulary = @vocabulary
717
- ec.name = name
718
- # ec.enforcement =
719
- role_sequences.each_with_index do |rs, i|
720
- @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
721
- end
722
- }
950
+ make_joins('equality', name, role_sequences)
951
+
952
+ ec = @constellation.SetEqualityConstraint(:new)
953
+ ec.vocabulary = @vocabulary
954
+ ec.name = name
955
+ # ec.enforcement =
956
+ role_sequences.each_with_index do |rs, i|
957
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
958
+ end
959
+ @by_id[id] = ec
960
+ }
961
+ end
723
962
  end
724
963
 
725
964
  def read_subset_constraints
726
965
  x_subset_constraints = @x_model.xpath("orm:Constraints/orm:SubsetConstraint")
727
- x_subset_constraints.each{|x|
728
- name = x["Name"] || ''
729
- name.gsub!(/\s/,'')
730
- name = nil if name.size == 0
731
- role_sequences =
732
- x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
733
- x_role_refs = x_rs.xpath("orm:Role")
734
- map_roles(
735
- x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
736
- "equality constraint #{name}"
737
- )
738
- }
739
-
740
- ec = @constellation.SubsetConstraint(:new)
741
- ec.vocabulary = @vocabulary
742
- ec.name = name
743
- # ec.enforcement =
744
- ec.subset_role_sequence = role_sequences[0]
745
- ec.superset_role_sequence = role_sequences[1]
746
- }
966
+ debug :orm, "Reading subset constraints" do
967
+ x_subset_constraints.each{|x|
968
+ id = x['id']
969
+ name = x["Name"] || ''
970
+ name = nil if name.size == 0
971
+ role_sequences =
972
+ x.xpath("orm:RoleSequences/orm:RoleSequence").map{|x_rs|
973
+ x_role_refs = x_rs.xpath("orm:Role")
974
+ map_roles(
975
+ x_role_refs , # .map{|xr| @x_by_id[xr['ref']] },
976
+ "equality constraint #{name}"
977
+ )
978
+ }
979
+ make_joins('subset', name, role_sequences)
980
+
981
+ ec = @constellation.SubsetConstraint(:new)
982
+ ec.vocabulary = @vocabulary
983
+ ec.name = name
984
+ # ec.enforcement =
985
+ ec.subset_role_sequence = role_sequences[0]
986
+ ec.superset_role_sequence = role_sequences[1]
987
+ @by_id[id] = ec
988
+ }
989
+ end
747
990
  end
748
991
 
749
992
  def read_ring_constraints
750
993
  x_ring_constraints = @x_model.xpath("orm:Constraints/orm:RingConstraint")
751
- x_ring_constraints.each{|x|
752
- name = x["Name"] || ''
753
- name.gsub!(/\s/,'')
754
- name = nil if name.size == 0
755
- type = x["Type"]
756
- # begin
757
- # # Convert the RingConstraint name to a number:
758
- # type_num = eval("::ActiveFacts::RingConstraint::#{type}")
759
- # rescue => e
760
- # throw "RingConstraint type #{type} isn't known"
761
- # end
994
+ debug :orm, "Reading ring constraints" do
995
+ x_ring_constraints.each{|x|
996
+ id = x['id']
997
+ name = x["Name"] || ''
998
+ name = nil if name.size == 0
999
+ ring_type = x["Type"]
762
1000
 
763
- from, to = *x.xpath("orm:RoleSequence/orm:Role").map{|xr|
764
- @by_id[xr['ref']]
765
- }
766
- rc = @constellation.RingConstraint(:new)
767
- rc.vocabulary = @vocabulary
768
- rc.name = name
769
- # rc.enforcement =
770
- rc.role = from
771
- rc.other_role = to
772
- rc.ring_type = type
773
- }
1001
+ from, to = *x.xpath("orm:RoleSequence/orm:Role").
1002
+ map do |xr|
1003
+ @by_id[xr['ref']]
1004
+ end
1005
+ if from.concept != to.concept
1006
+ join_over, = *ActiveFacts::Metamodel.join_roles_over([from, to], :counterpart)
1007
+ raise "Ring constraint has incompatible players #{from.concept.name}, #{to.concept.name}" if !join_over
1008
+ debug :join, "join ring constraint over #{join_over.name}"
1009
+ end
1010
+ rc = @constellation.RingConstraint(:new)
1011
+ rc.vocabulary = @vocabulary
1012
+ rc.name = name
1013
+ # rc.enforcement =
1014
+ rc.role = from
1015
+ rc.other_role = to
1016
+ rc.ring_type = ring_type.gsub(/PurelyReflexive/,'Reflexive')
1017
+ @by_id[id] = rc
1018
+ }
1019
+ end
774
1020
  end
775
1021
 
776
1022
  def read_frequency_constraints
777
1023
  x_frequency_constraints = @x_model.xpath("orm:Constraints/orm:FrequencyConstraint")
778
- # REVISIT: FrequencyConstraints not handled yet
1024
+ debug :orm, "Reading frequency constraints" do
1025
+ x_frequency_constraints.each do |x_frequency_constraint|
1026
+ id = x_frequency_constraint['id']
1027
+ min_frequency = x_frequency_constraint["MinFrequency"].to_i
1028
+ min_frequency = nil if min_frequency == 0
1029
+ max_frequency = x_frequency_constraint["MaxFrequency"].to_i
1030
+ max_frequency = nil if max_frequency == 0
1031
+ x_roles = x_frequency_constraint.xpath("orm:RoleSequence/orm:Role")
1032
+ role = @by_id[x_roles[0]["ref"]]
1033
+ role_sequence = @constellation.RoleSequence(:new)
1034
+ role_ref = @constellation.RoleRef(role_sequence, 0, :role => role)
1035
+ debug :orm, "FrequencyConstraint(min #{min_frequency.inspect} max #{max_frequency.inspect} over #{role.fact_type.describe(role)} #{id} role ref = #{x_roles[0]["ref"]}"
1036
+ @by_id[id] = @constellation.PresenceConstraint(
1037
+ :new,
1038
+ :vocabulary => @vocabulary,
1039
+ :name => name = x_frequency_constraint["Name"] || '',
1040
+ :role_sequence => role_sequence,
1041
+ :is_mandatory => false,
1042
+ :min_frequency => min_frequency,
1043
+ :max_frequency => max_frequency,
1044
+ :is_preferred_identifier => false
1045
+ )
1046
+ end
1047
+ end
779
1048
  end
780
1049
 
781
1050
  def read_instances
782
- population = Population.new(@vocabulary, "sample")
783
-
784
- # Value instances first, then entities then facts:
785
-
786
- x_values = @x_model.xpath("orm:Objects/orm:ValueType/orm:Instances/orm:ValueTypeInstance/orm:Value")
787
- #pp x_values.map{|v| [ v.parent['id'], v.text ] }
788
- x_values.each{|v|
789
- id = v.parent['id']
790
- # Get details of the ValueType:
791
- xvt = v.parent.parent.parent
792
- vt_id = xvt['id']
793
- vtname = xvt['Name'] || ''
794
- vtname.gsub!(/\s/,'')
795
- vtname = nil if name.size == 0
796
- vt = @by_id[vt_id]
797
- throw "ValueType #{vtname} not found" unless vt
798
-
799
- i = Instance.new(vt, [v.text, is_a_string(v.text), nil])
800
- @by_id[id] = i
801
- # show_xmlobj(v)
802
- }
1051
+ debug :orm, "Reading sample data" do
1052
+ population = @constellation.Population(@vocabulary, "sample")
1053
+
1054
+ # Value instances first, then entities then facts:
1055
+
1056
+ x_values = @x_model.xpath("orm:Objects/orm:ValueType/orm:Instances/orm:ValueTypeInstance/orm:Value")
1057
+ #pp x_values.map{|v| [ v.parent['id'], v.text ] }
1058
+ debug :orm, "Reading sample values" do
1059
+ x_values.each{|v|
1060
+ id = v.parent['id']
1061
+ # Get details of the ValueType:
1062
+ xvt = v.parent.parent.parent
1063
+ vt_id = xvt['id']
1064
+ vtname = xvt['Name'] || ''
1065
+ #vtname.gsub!(/\s/,'')
1066
+ vtname = nil if vtname.size == 0
1067
+ vt = @by_id[vt_id]
1068
+ throw "ValueType #{vtname} not found" unless vt
1069
+
1070
+ i = @constellation.Instance(:new, :population => population, :concept => vt, :value => [v.text, is_a_string(v.text), nil])
1071
+ @by_id[id] = i
1072
+ # show_xmlobj(v)
1073
+ }
1074
+ end
1075
+
1076
+ # Use the "id" attribute of EntityTypeInstance
1077
+ x_entities = @x_model.xpath("orm:Objects/orm:EntityType/orm:Instances/orm:EntityTypeInstance")
1078
+ #pp x_entities
1079
+ # x_entities.each{|v| show_xmlobj(v) }
1080
+ last_et_id = nil
1081
+ last_et = nil
1082
+ et = nil
1083
+ debug :orm, "Reading sample entities" do
1084
+ x_entities.each{|v|
1085
+ id = v['id']
1086
+
1087
+ # Get details of the EntityType:
1088
+ xet = v.parent.parent
1089
+ et_id = xet['id']
1090
+ if (et_id != last_et_id)
1091
+ etname = xet['Name'] || ''
1092
+ #etname.gsub!(/\s/,'')
1093
+ etname = nil if etname.size == 0
1094
+ last_et = et = @by_id[et_id]
1095
+ last_et_id = et_id
1096
+ throw "EntityType #{etname} not found" unless et
1097
+ end
803
1098
 
804
- # Use the "id" attribute of EntityTypeInstance
805
- x_entities = @x_model.xpath("orm:Objects/orm:EntityType/orm:Instances/orm:EntityTypeInstance")
806
- #pp x_entities
807
- # x_entities.each{|v| show_xmlobj(v) }
808
- last_et_id = nil
809
- last_et = nil
810
- et = nil
811
- x_entities.each{|v|
812
- id = v['id']
813
-
814
- # Get details of the EntityType:
815
- xet = v.parent.parent
816
- et_id = xet['id']
817
- if (et_id != last_et_id)
818
- etname = xet['Name'] || ''
819
- etname.gsub!(/\s/,'')
820
- etname = nil if name.size == 0
821
- last_et = et = @by_id[et_id]
822
- last_et_id = et_id
823
- throw "EntityType #{etname} not found" unless et
1099
+ instance = @constellation.Instance(:new, :population => population, :concept => et, :value => nil)
1100
+ @by_id[id] = instance
1101
+ debug :orm, "Made new EntityType #{etname}"
1102
+ }
824
1103
  end
825
1104
 
826
- instance = Instance.new(et, nil)
827
- @by_id[id] = instance
828
- # puts "Made new EntityType #{etname}"
829
- }
1105
+ # The EntityType instances have implicit facts for the PI facts.
1106
+ # We must create implicit PI facts after all the instances.
1107
+ entity_count = 0
1108
+ pi_fact_count = 0
1109
+ debug :orm, "Creating identifying facts for entities" do
1110
+ x_entities.each do |v|
1111
+ id = v['id']
1112
+ instance = @by_id[id]
1113
+ et = @by_id[v.parent.parent['id']]
1114
+ next unless (preferred_id = et.preferred_identifier)
1115
+
1116
+ debug :orm, "Create identifying facts using #{preferred_id}"
1117
+
1118
+ # Collate the referenced objects by role:
1119
+ role_instances = v.elements[0].elements.inject({}){|h, v|
1120
+ etri = @x_by_id[v['ref']]
1121
+ x_role_id = etri.parent.parent['id']
1122
+ role = @by_id[x_role_id]
1123
+ object = @by_id[object_id = etri['ref']]
1124
+ h[role] = object
1125
+ h
1126
+ }
1127
+
1128
+ # Create an instance of each required fact type, for compound identification:
1129
+ identifying_fact_types =
1130
+ preferred_id.role_sequence.all_role_ref.map { |rr| rr.role.fact_type }.uniq
1131
+ identifying_fact_types.
1132
+ each do |ft|
1133
+ debug :orm, "For FactType #{ft}" do
1134
+ fact = @constellation.Fact(:new, :population => population, :fact_type => ft)
1135
+ fact_roles = ft.all_role.map do |role|
1136
+ if role.concept == et
1137
+ object = instance
1138
+ else
1139
+ object = role_instances[role]
1140
+ debug :orm, "instance for role #{role} is #{object}"
1141
+ end
1142
+ @constellation.RoleValue(:instance => object, :population => population, :fact => fact, :role => role)
1143
+ end
1144
+ end
1145
+ pi_fact_count += 1
1146
+ end
1147
+
1148
+ entity_count += 1
1149
+ end
1150
+ end
1151
+ debug :orm, "Created #{pi_fact_count} facts to identify #{entity_count} entities"
1152
+
1153
+ # Use the "ref" attribute of FactTypeRoleInstance:
1154
+ x_fact_roles = @x_model.xpath("orm:Facts/orm:Fact/orm:Instances/orm:FactTypeInstance/orm:RoleInstances/orm:FactTypeRoleInstance")
1155
+
1156
+ last_id = nil
1157
+ fact = nil
1158
+ fact_roles = []
1159
+ debug :orm, "Reading sample facts" do
1160
+ x_fact_roles.each do |v|
1161
+ fact_type_id = v.parent.parent.parent.parent['id']
1162
+ id = v.parent.parent['id']
1163
+ fact_type = @by_id[fact_type_id]
1164
+ throw "Fact type #{fact_type_id} not found" unless fact_type
830
1165
 
831
- # The EntityType instances have implicit facts for the PI facts.
832
- # We must create implicit PI facts after all the instances.
833
- entity_count = 0
834
- pi_fact_count = 0
835
- x_entities.each{|v|
836
- id = v['id']
837
- instance = @by_id[id]
838
- et = @by_id[v.parent.parent['id']]
839
- next unless (preferred_id = et.preferred_identifier)
840
-
841
- # puts "Create identifying facts using #{preferred_id}"
842
-
843
- # Collate the referenced objects by role:
844
- role_instances = v.elements[0].elements.inject({}){|h, v|
845
- etri = @x_by_id[v['ref']]
846
- x_role_id = etri.parent.parent['id']
1166
+ # Create initial and subsequent Fact objects:
1167
+ fact = @constellation.Fact(:new, :population => population, :fact_type => fact_type) unless fact && last_id == id
1168
+ last_id = id
1169
+
1170
+ # REVISIT: This doesn't handle instances of objectified fact types (where a RoleValue.instance objectifies Fact)
1171
+
1172
+ x_role_instance = @x_by_id[v['ref']]
1173
+ x_role_id = x_role_instance.parent.parent['id']
847
1174
  role = @by_id[x_role_id]
848
- object = @by_id[object_id = etri['ref']]
849
- h[role] = object
850
- h
851
- }
1175
+ throw "Role not found for instance #{x_role_id}" unless role
1176
+ instance_id = x_role_instance['ref']
1177
+ instance = @by_id[instance_id]
1178
+ throw "Instance not found for FactRole #{instance_id}" unless instance
1179
+ @constellation.RoleValue(:instance => instance, :population => population, :fact => fact, :role => role)
1180
+ end
1181
+ end
852
1182
 
853
- # Create an instance of each required fact type, for compound identification:
854
- preferred_id.role_sequence.map(&:fact_type).uniq.each{|ft|
855
- # puts "\tFor FactType #{ft}"
856
- fact_roles = ft.roles.map{|role|
857
- if role.concept == et
858
- object = instance
859
- else
860
- object = role_instances[role]
861
- # puts "\t\tinstance for role #{role} is #{object}"
1183
+ end
1184
+ end
1185
+
1186
+ def read_diagrams
1187
+ x_diagrams = @document.root.xpath("ormDiagram:ORMDiagram")
1188
+ debug :orm, "Reading diagrams" do
1189
+ x_diagrams.each do |x|
1190
+ name = (x["Name"] || '').strip
1191
+ diagram = @constellation.Diagram(@vocabulary, name)
1192
+ debug :diagram, "Starting to read diagram #{name}"
1193
+ shapes = x.xpath("ormDiagram:Shapes/*")
1194
+ debug :orm, "Reading shapes" do
1195
+ shapes.map do |x_shape|
1196
+ x_subject = x_shape.xpath("ormDiagram:Subject")[0]
1197
+ subject = @by_id[x_subject["ref"]]
1198
+ is_expanded = v = x_shape['IsExpanded'] and v == 'true'
1199
+ bounds = x_shape['AbsoluteBounds']
1200
+ case shape_type = x_shape.name
1201
+ when 'FactTypeShape'
1202
+ read_fact_type_shape diagram, x_shape, is_expanded, bounds, subject
1203
+ when 'ExternalConstraintShape', 'FrequencyConstraintShape'
1204
+ # REVISIT: The offset might depend on the constraint type. This is right for subset and other round ones.
1205
+ position = convert_position(bounds, Gravity::NW, 31, 31)
1206
+ shape = @constellation.ConstraintShape(
1207
+ :new, :diagram => diagram, :position => position, :is_expanded => is_expanded,
1208
+ :constraint => subject
1209
+ )
1210
+ when 'RingConstraintShape'
1211
+ # REVISIT: The offset might depend on the ring constraint type. This is right for basic round ones.
1212
+ position = convert_position(bounds, Gravity::NW, 31, 31)
1213
+ shape = @constellation.RingConstraintShape(
1214
+ :new, :diagram => diagram, :position => position, :is_expanded => is_expanded,
1215
+ :constraint => subject
1216
+ )
1217
+ shape.fact_type = subject.role.fact_type
1218
+ when 'ModelNoteShape'
1219
+ # REVISIT: Add model notes
1220
+ when 'ObjectTypeShape'
1221
+ shape = @constellation.ObjectTypeShape(
1222
+ :new, :diagram => diagram, :position => position, :is_expanded => is_expanded,
1223
+ :concept => subject,
1224
+ :has_expanded_reference_mode => false # REVISIT
1225
+ )
1226
+ else
1227
+ raise "Unknown shape #{x_shape.name}"
1228
+ end
862
1229
  end
863
- FactRole.new(role, object)
864
- }
865
- f = Fact.new(population, ft, *fact_roles)
866
- pi_fact_count += 1
867
- }
868
- entity_count += 1
869
- }
870
- # puts "Created #{pi_fact_count} facts to identify #{entity_count} entities"
871
-
872
- # Use the "ref" attribute of FactTypeRoleInstance:
873
- x_fact_roles = @x_model.xpath("orm:Facts/orm:Fact/orm:Instances/orm:FactTypeInstance/orm:RoleInstances/orm:FactTypeRoleInstance")
874
-
875
- last_id = nil
876
- last_fact_type = nil
877
- fact_roles = []
878
- x_fact_roles.each{|v|
879
- fact_type_id = v.parent.parent.parent.parent['id']
880
- id = v.parent.parent['id']
881
- fact_type = @by_id[fact_type_id]
882
- throw "Fact type #{fact_type_id} not found" unless fact_type
883
-
884
- if (last_id && id != last_id)
885
- # Process completed fact now we have all roles:
886
- last_fact = Fact.new(population, last_fact_type, *fact_roles)
887
- fact_roles = []
888
- else
889
- last_fact_type = fact_type
1230
+ end
890
1231
  end
1232
+ end
1233
+ end
891
1234
 
892
- #show_xmlobj(v)
893
-
894
- last_id = id
895
- x_role_instance = @x_by_id[v['ref']]
896
- x_role_id = x_role_instance.parent.parent['id']
897
- role = @by_id[x_role_id]
898
- throw "Role not found for instance #{x_role_id}" unless role
899
- instance_id = x_role_instance['ref']
900
- instance = @by_id[instance_id]
901
- throw "Instance not found for FactRole #{instance_id}" unless instance
902
- fact_roles << FactRole.new(role, instance)
903
- }
1235
+ def read_fact_type_shape diagram, x_shape, is_expanded, bounds, fact_type
1236
+ display_role_names_setting = v = x_shape["DisplayRoleNames"] and
1237
+ case v
1238
+ when 'Off'; 'false'
1239
+ when 'On'; 'true'
1240
+ else nil
1241
+ end
1242
+ rotation_setting = v = x_shape['DisplayOrientation'] and
1243
+ case v
1244
+ when 'VerticalRotatedLeft'; 'left'
1245
+ when 'VerticalRotatedRight'; 'right'
1246
+ else nil
1247
+ end
1248
+ # Position of a fact type is the top-left of the first role box
1249
+ offs_x = 0
1250
+ offs_y = 0
1251
+ if fact_type.entity_type # If objectified, move right 12, down 24
1252
+ offs_x += 12
1253
+ offs_y += 24
1254
+ end
1255
+
1256
+ # count internal UC's, add 27 Y units for each:
1257
+ iucs = fact_type.internal_presence_constraints.select{|uc| uc.max_frequency == 1 }
1258
+ offs_y += iucs.size*27
1259
+ position = convert_position(bounds, Gravity::NW, offs_x, offs_y)
1260
+ debug :orm, "REVISIT: Can't place rotated fact type correctly on diagram yet" if rotation_setting
1261
+
1262
+ debug :orm, "fact type at #{position.x},#{position.y} has display_role_names_setting=#{display_role_names_setting.inspect}, rotation_setting=#{rotation_setting.inspect}, #{iucs.size} IUC's"
1263
+ shape = @constellation.FactTypeShape(
1264
+ :new,
1265
+ :diagram => diagram,
1266
+ :position => position,
1267
+ :is_expanded => is_expanded,
1268
+ :display_role_names_setting => display_role_names_setting,
1269
+ :rotation_setting => rotation_setting,
1270
+ :fact_type => fact_type
1271
+ )
1272
+ # Create RoleDisplay objects if necessary
1273
+ x_role_display = x_shape.xpath("ormDiagram:RoleDisplayOrder/ormDiagram:Role")
1274
+ # print "Fact type '#{fact_type.preferred_reading.expand}' (#{fact_type.all_role.map{|r|r.concept.name}*' '})"
1275
+ if x_role_display.size > 0
1276
+ debug :orm, " has roleDisplay (#{x_role_display.map{|rd| @by_id[rd['ref']].concept.name}*','})'"
1277
+ x_role_display.each_with_index do |rd, ordinal|
1278
+ role_display = @constellation.RoleDisplay(shape, ordinal, :role => @by_id[rd['ref']])
1279
+ end
1280
+ else
1281
+ # Decide whether to create all RoleDisplay objects for this fact type, which is in role order
1282
+ # Omitting this here might lead to incomplete RoleDisplay sequences,
1283
+ # because each RoleNameShape or ValueConstraintShape creates just one.
1284
+ debug :orm, " has no roleDisplay"
1285
+ end
904
1286
 
905
- if (last_id)
906
- # Process final completed fact now we have all roles:
907
- last_fact = Fact.new(population, last_fact_type, *fact_roles)
1287
+ relative_shapes = x_shape.xpath('ormDiagram:RelativeShapes/*')
1288
+ relative_shapes.each do |xr_shape|
1289
+ position = convert_position(xr_shape['AbsoluteBounds'])
1290
+ case xr_shape.name
1291
+ when 'ObjectifiedFactTypeNameShape'
1292
+ @constellation.ObjectifiedFactTypeNameShape(shape, :diagram => diagram, :position => position, :is_expanded => false)
1293
+ when 'ReadingShape'
1294
+ @constellation.ReadingShape(:new, :diagram => diagram, :position => position, :is_expanded => false, :reading => fact_type.preferred_reading)
1295
+ when 'RoleNameShape'
1296
+ role = @by_id[xr_shape.xpath("ormDiagram:Subject")[0]['ref']]
1297
+ role_display = role_display_for_role(shape, x_role_display, role)
1298
+ debug :orm, "Fact type '#{fact_type.preferred_reading.expand}' has #{xr_shape.name}"
1299
+ @constellation.RoleNameShape(
1300
+ :new, :diagram => diagram, :position => position, :is_expanded => false,
1301
+ :role_display => role_display
1302
+ )
1303
+ when 'ValueConstraintShape'
1304
+ vc_subject_id = xr_shape.xpath("ormDiagram:Subject")[0]['ref']
1305
+ constraint = @by_id[vc_subject_id]
1306
+ debug :orm, "Fact type '#{fact_type.preferred_reading.expand}' has #{xr_shape.name} for #{constraint.inspect}"
1307
+
1308
+ role_display = role_display_for_role(shape, x_role_display, constraint.role)
1309
+ debug :orm, "ValueConstraintShape is on #{role_ordinal}'th role (by #{x_role_display.size > 0 ? 'role_display' : 'fact roles'})"
1310
+ @constellation.ValueConstraintShape(
1311
+ :new, :diagram => diagram, :position => position, :is_expanded => false,
1312
+ :constraint => constraint,
1313
+ :object_type_shape => nil, # This constraint is relative to a Fact Type, so must be on a role
1314
+ :role_display => role_display
1315
+ )
1316
+ else raise "Unknown relative shape #{xr_shape.name}"
1317
+ end
1318
+ end
1319
+ end
1320
+
1321
+ # Find or create the RoleDisplay for this role in this fact_type_shape, given (possibly empty) x_role_display nodes:
1322
+ def role_display_for_role(fact_type_shape, x_role_display, role)
1323
+ if x_role_display.size == 0
1324
+ role_ordinal = fact_type_shape.fact_type.all_role.to_a.index(role)
1325
+ else
1326
+ role_ordinal = x_role_display.map{|rd| @by_id[rd['ref']]}.index(role)
908
1327
  end
1328
+ role_display = @constellation.RoleDisplay(fact_type_shape, role_ordinal, :role => role)
1329
+ end
909
1330
 
1331
+ DIAGRAM_SCALE = 384
1332
+ def convert_position(bounds, gravity = Gravity::NW, xoffs = 0, yoffs = 0)
1333
+ return nil unless bounds
1334
+ bf = bounds.split(/, /).map{|b|b.to_f}
1335
+ sizefrax = [
1336
+ [0, 0], [1, 0], [2, 0],
1337
+ [0, 1], [1, 1], [2, 2],
1338
+ [0, 2], [1, 2], [2, 2],
1339
+ ]
1340
+
1341
+ x = (DIAGRAM_SCALE * bf[0]+bf[2]*sizefrax[gravity][0]/2).round + xoffs
1342
+ y = (DIAGRAM_SCALE * bf[1]+bf[3]*sizefrax[gravity][1]/2).round + yoffs
1343
+ @constellation.Position(x, y)
910
1344
  end
911
1345
 
912
1346
  # Detect numeric data and denote it as a string: