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
@@ -1,403 +0,0 @@
1
- #
2
- # ActiveFacts Generators.
3
- # Generate HTML-highlighted CQL from an ActiveFacts vocabulary.
4
- #
5
- # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
- #
7
- # The text generated here is pre-formatted, and it spans haing the following styles:
8
- # keyword: ORM2 standard colour is #00C (blue)
9
- # concept: ORM2 standard concept is #808 (purple)
10
- # copula: ORM2 standard concept is #060 (green)
11
- #
12
- require 'activefacts/vocabulary'
13
- require 'activefacts/api'
14
- require 'activefacts/generate/ordered'
15
- require 'activefacts/generate/cql'
16
-
17
- module ActiveFacts
18
- module Generate
19
- class CQL
20
- # Generate CQL with HTML syntax-highlighting for an ActiveFacts vocabulary.
21
- # Invoke as
22
- # afgen --cql/html <file>.cql
23
- class HTML < CQL
24
- private
25
-
26
- def initialize(vocabulary, *options)
27
- super
28
- end
29
-
30
- def puts s
31
- super(s.gsub(/[,;]/) do |p| keyword p; end)
32
- end
33
-
34
- def keyword(str)
35
- "<span class='keyword'>#{str}</span>"
36
- end
37
-
38
- def concept(str)
39
- "<span class='concept'>#{str}</span>"
40
- end
41
-
42
- def copula(str)
43
- "<span class='copula'>#{str}</span>"
44
- end
45
-
46
- def vocabulary_start(vocabulary)
47
- puts %q{<head>
48
- <link rel="stylesheet" href="css/orm2.css" type="text/css"/>
49
- </head>
50
- <pre class="copula">}
51
- puts "#{keyword "vocabulary"} #{concept(vocabulary.name)};\n\n"
52
- end
53
-
54
- def vocabulary_end
55
- puts %q{</pre>}
56
- end
57
-
58
- def value_type_banner
59
- puts "/*\n * Value Types\n */"
60
- end
61
-
62
- def value_type_dump(o)
63
- return unless o.supertype # An imported type
64
- if o.name == o.supertype.name
65
- # In ActiveFacts, parameterising a ValueType will create a new ValueType
66
- # throw Can't handle parameterized value type of same name as its ValueType" if ...
67
- end
68
-
69
- parameters =
70
- [ o.length != 0 || o.scale != 0 ? o.length : nil,
71
- o.scale != 0 ? o.scale : nil
72
- ].compact
73
- parameters = parameters.length > 0 ? "("+parameters.join(",")+")" : "()"
74
-
75
- puts "#{concept o.name} #{keyword "is written as"} #{concept o.supertype.name + parameters }#{
76
- if (o.value_restriction)
77
- keyword("restricted to")+
78
- o.value_restriction.all_allowed_range.map{|ar|
79
- # REVISIT: Need to display as string or numeric according to type here...
80
- min = ar.value_range.minimum_bound
81
- max = ar.value_range.maximum_bound
82
-
83
- range = (min ? min.value : "") +
84
- (min.value != (max&&max.value) ? (".." + (max ? max.value : "")) : "")
85
- keyword range
86
- }*", "
87
- else
88
- ""
89
- end
90
- };"
91
- end
92
-
93
- def append_ring_to_reading(reading, ring)
94
- reading << keyword(" [#{(ring.ring_type.scan(/[A-Z][a-z]*/)*", ").downcase}]")
95
- end
96
-
97
- def identified_by_roles_and_facts(entity_type, identifying_roles, identifying_facts, preferred_readings)
98
- identifying_role_names = identifying_roles.map{|role|
99
- preferred_role_ref = preferred_readings[role.fact_type].role_sequence.all_role_ref.detect{|reading_rr|
100
- reading_rr.role == role
101
- }
102
- role_words = []
103
- # REVISIT: Consider whether NOT to use the adjective if it's a prefix of the role_name
104
-
105
- role_name = role.role_name
106
- role_name = nil if role_name == ""
107
- # debug "concept.name=#{preferred_role_ref.role.concept.name}, role_name=#{role_name.inspect}, preferred_role_name=#{preferred_role_ref.role.role_name.inspect}"
108
-
109
- if (role.fact_type.all_role.size == 1)
110
- # REVISIT: Guard against unary reading containing the illegal words "and" and "where".
111
- role.fact_type.default_reading # Need whole reading for a unary.
112
- elsif (role_name)
113
- role_name
114
- else
115
- role_words << preferred_role_ref.leading_adjective if preferred_role_ref.leading_adjective != ""
116
- role_words << preferred_role_ref.role.concept.name
117
- role_words << preferred_role_ref.trailing_adjective if preferred_role_ref.trailing_adjective != ""
118
- role_words.compact*"-"
119
- end
120
- }
121
-
122
- # REVISIT: Consider emitting extra fact types here, instead of in entity_type_dump?
123
- # Just beware that readings having the same players will be considered to be of the same fact type, even if they're not.
124
-
125
- # Detect standard reference-mode scenarios
126
- ft = identifying_facts[0]
127
- fact_constraints = nil
128
- if identifying_facts.size == 1 and
129
- entity_role = ft.all_role[n = (ft.all_role[0].concept == entity_type ? 0 : 1)] and
130
- value_role = ft.all_role[1-n] and
131
- value_name = value_role.concept.name and
132
- residual = value_name.gsub(%r{#{entity_role.concept.name}},'') and
133
- residual != '' and
134
- residual != value_name
135
-
136
- # The EntityType is identified by its association with a single ValueType
137
- # whose name is an extension (the residual) of the EntityType's name.
138
-
139
- # Detect standard reference-mode readings:
140
- forward_reading = reverse_reading = nil
141
- ft.all_reading.each do |reading|
142
- if reading.text =~ /^\{(\d)\} has \{\d\}$/
143
- if reading.role_sequence.all_role_ref[$1.to_i].role == entity_role
144
- forward_reading = reading
145
- else
146
- reverse_reading = reading
147
- end
148
- elsif reading.text =~ /^\{(\d)\} is of \{\d\}$/
149
- if reading.role_sequence.all_role_ref[$1.to_i].role == value_role
150
- reverse_reading = reading
151
- else
152
- forward_reading = reading
153
- end
154
- end
155
- end
156
-
157
- debug :mode, "------------------- Didn't find standard forward reading" unless forward_reading
158
- debug :mode, "------------------- Didn't find standard reverse reading" unless reverse_reading
159
-
160
- # If we didn't find at least one of the standard readings, don't use a refmode:
161
- if (forward_reading || reverse_reading)
162
- # Elide the constraints that would have been emitted on those readings.
163
- # If there is a UC that's not in the standard form for a reference mode,
164
- # we have to emit the standard reading anyhow.
165
- fact_constraints = @presence_constraints_by_fact[ft]
166
- fact_constraints.each do |pc|
167
- if (pc.role_sequence.all_role_ref.size == 1 and pc.max_frequency == 1)
168
- # It's a uniqueness constraint, and will be regenerated
169
- @constraints_used[pc] = true
170
- end
171
- end
172
-
173
- @fact_types_dumped[ft] = true
174
-
175
- # Figure out whether any non-standard readings exist:
176
- other_readings = ft.all_reading - [forward_reading] - [reverse_reading]
177
- debug :mode, "--- other_readings.size now = #{other_readings.size}" if other_readings.size > 0
178
-
179
- fact_text = other_readings.map do |reading|
180
- expanded_reading(reading, fact_constraints, true)
181
- end*",\n\t"
182
- return keyword(" identified by its ") +
183
- concept(residual) +
184
- (fact_text != "" ? keyword(" where\n\t") + fact_text : "")
185
- end
186
- end
187
-
188
- identifying_facts.each{|f| @fact_types_dumped[f] = true }
189
- @identifying_fact_text =
190
- identifying_facts.map{|f|
191
- fact_readings_with_constraints(f, fact_constraints)
192
- }.flatten*",\n\t"
193
-
194
- keyword(" identified by ") +
195
- identifying_role_names.map{|n| concept n} * keyword(" and ") +
196
- keyword(" where\n\t") +
197
- @identifying_fact_text
198
- end
199
-
200
- def show_frequency role, constraint
201
- # REVISIT: Need to also colorize the adjectives here:
202
- [ constraint ? keyword(constraint.frequency) : nil, concept(role.concept.name) ]
203
- end
204
-
205
- def entity_type_banner
206
- puts(keyword("/*\n * Entity Types\n */"))
207
- end
208
-
209
- def fact_readings(fact_type)
210
- constrained_fact_readings = fact_readings_with_constraints(fact_type)
211
- constrained_fact_readings*",\n\t"
212
- end
213
-
214
- def subtype_dump(o, supertypes, pi)
215
- print "#{concept o.name} #{keyword "is a kind of"} #{ o.supertypes.map(&:name).map{|n| concept n}*keyword(", ") }"
216
- if pi
217
- print identified_by(o, pi)
218
- end
219
- # If there's a preferred_identifier for this subtype, identifying readings were emitted
220
- if o.fact_type
221
- print(
222
- (pi ? "," : keyword(" where")) +
223
- "\n\t" +
224
- fact_readings(o.fact_type)
225
- )
226
- end
227
- puts ";\n"
228
- end
229
-
230
- def non_subtype_dump(o, pi)
231
- print "#{concept(o.name)} #{keyword "is"}" +
232
- identified_by(o, pi)
233
- print(keyword(" where\n\t") + fact_readings(o.fact_type)) if o.fact_type
234
- puts ";\n"
235
- end
236
-
237
- def fact_type_dump(fact_type, name)
238
-
239
- @identifying_fact_text = nil
240
- if (o = fact_type.entity_type)
241
- print "#{concept o.name} #{keyword "is"}"
242
- if !o.all_type_inheritance_as_subtype.empty?
243
- print(keyword(" a kind of ") + o.supertypes.map(&:name).map{|n| concept n}*", ")
244
- end
245
-
246
- # Alternate identification of objectified fact type?
247
- primary_supertype = o.supertypes[0]
248
- pi = fact_type.entity_type.preferred_identifier
249
- if pi && primary_supertype && primary_supertype.preferred_identifier != pi
250
- print identified_by(o, pi)
251
- print ";\n"
252
- end
253
- end
254
-
255
- unless @identifying_fact_text
256
- print(keyword(" where\n\t")) if o
257
- puts(fact_readings(fact_type)+";")
258
- end
259
- end
260
-
261
- def fact_type_banner
262
- puts keyword("/*\n * Fact Types\n */")
263
- end
264
-
265
- def constraint_banner
266
- puts keyword("/*\n * Constraints:\n */")
267
- end
268
-
269
- def dump_presence_constraint(c)
270
- roles = c.role_sequence.all_role_ref.map{|rr| rr.role }
271
-
272
- # REVISIT: If only one role is covered and it's mandatory >=1 constraint, use SOME/THAT form:
273
- # each Bug SOME Tester logged THAT Bug;
274
- players = c.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}.uniq
275
-
276
- fact_types = c.role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq
277
- puts \
278
- "#{keyword "each #{players.size > 1 ? "combination " : ""}"}"+
279
- "#{players.map{|n| concept n}*", "} "+
280
- "#{keyword "occurs #{c.frequency} time in"}\n\t"+
281
- "#{fact_types.map{|ft| ft.default_reading([], nil)}*",\n\t"}" +
282
- ";"
283
- end
284
-
285
- def dump_set_constraint(c)
286
- # REVISIT exclusion: every <player-list> must<?> either reading1, reading2, ...
287
-
288
- # Each constraint involves two or more occurrences of one or more players.
289
- # For each player, a subtype may be involved in the occurrences.
290
- # Find the common supertype of each player.
291
- scrs = c.all_set_comparison_roles
292
- player_count = scrs[0].role_sequence.all_role_ref.size
293
- role_seq_count = scrs.size
294
-
295
- #raise "Can't verbalise constraint over many players and facts" if player_count > 1 and role_seq_count > 1
296
-
297
- players_differ = [] # Record which players are also played by subclasses
298
- players = (0...player_count).map do |pi|
299
- # Find the common supertype of the players of the pi'th role in each sequence
300
- concepts = scrs.map{|r| r.role_sequence.all_role_ref[pi].role.concept }
301
- player, players_differ[pi] = common_supertype(concepts)
302
- raise "Role sequences of #{c.class.basename} must have concepts matching #{c.name} in position #{pi}" unless player
303
- player
304
- end
305
- #puts "#{c.class.basename} has players #{players.map{|p| p.name}*", "}"
306
-
307
- if (SetEqualityConstraint === c)
308
- # REVISIT: Need a proper approach to some/that and adjective disambiguation:
309
- puts \
310
- scrs.map{|scr|
311
- scr.role_sequence.all_role_ref.map{|rr|
312
- rr.role.fact_type.default_reading([], nil)
313
- }*keyword(" and ")
314
- } * keyword("\n\tif and only if\n\t") + ";"
315
- return
316
- end
317
-
318
- mode = c.is_mandatory ? "exactly one" : "at most one"
319
- puts "#{keyword "for each"} #{players.map{|p| concept p.name}*", "} #{keyword(mode + " of these holds")}:\n\t" +
320
- (scrs.map do |scr|
321
- constrained_roles = scr.role_sequence.all_role_ref.map{|rr| rr.role }
322
- fact_types = constrained_roles.map{|r| r.fact_type }.uniq
323
-
324
- fact_types.map do |fact_type|
325
- # REVISIT: future: Use "THAT" and "SOME" only when:
326
- # - the role player occurs twice in the reading, or
327
- # - is a subclass of the constrained concept, or
328
- reading = fact_type.preferred_reading
329
- expand_constrained(reading, constrained_roles, players, players_differ)
330
- end * keyword(" and ")
331
-
332
- end*",\n\t"
333
- )+';'
334
- end
335
-
336
- # Expand this reading using (in)definite articles where needed
337
- # Handle any roles in constrained_roles specially.
338
- def expand_constrained(reading, constrained_roles, players, players_differ)
339
- frequency_constraints = reading.role_sequence.all_role_ref.map {|role_ref|
340
- i = constrained_roles.index(role_ref.role)
341
- if !i
342
- v = [ "some", role_ref.role.concept.name]
343
- elsif players_differ[i]
344
- v = [ "that", players[i].name ] # Make sure to use the superclass name
345
- else
346
- if reading.fact_type.all_role.select{|r| r.concept == role_ref.role.concept }.size > 1
347
- v = [ "that", role_ref.role.concept.name ]
348
- else
349
- v = [ "some", role_ref.role.concept.name ]
350
- end
351
- end
352
-
353
- v[0] = keyword(v[0])
354
- v[1] = concept(v[1])
355
- v
356
- }
357
- frequency_constraints = [] unless frequency_constraints.detect{|fc| fc[0] =~ /some/ }
358
-
359
- #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
360
-
361
- # REVISIT: Make sure that we refer to the constrained players by their common supertype
362
-
363
- reading.expand(frequency_constraints, nil)
364
- end
365
-
366
- def dump_subset_constraint(c)
367
- # If the role players are identical and not duplicated, we can simply say "reading1 only if reading2"
368
- subset_roles = c.subset_role_sequence.all_role_ref.map{|rr| rr.role}
369
- superset_roles = c.superset_role_sequence.all_role_ref.map{|rr| rr.role}
370
-
371
- subset_players = subset_roles.map(&:concept)
372
- superset_players = superset_roles.map(&:concept)
373
-
374
- subset_fact_types = c.subset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type }.uniq
375
- superset_fact_types = c.superset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type }.uniq
376
-
377
- # We need to ensure that if the player of any constrained role also exists
378
- # as the player of a role that's not a constrained role, there are different
379
- # adjectives or other qualifiers qualifier applied to distinguish that role.
380
- fact_type_roles = (subset_fact_types+superset_fact_types).map{|ft| ft.all_role }.flatten
381
- non_constrained_roles = fact_type_roles - subset_roles - superset_roles
382
- if (r = non_constrained_roles.detect{|r| (subset_roles+superset_roles).include?(r) })
383
- # REVISIT: Find a way to deal with this problem, should it arise.
384
-
385
- # It would help, but not entirely fix it, to use SOME/THAT to identify the constrained roles.
386
- # See ServiceDirector's DataStore<->Client fact types for example
387
- # Use SOME on the subset, THAT on the superset.
388
- raise "Critical ambiguity, #{r.concept.name} occurs both constrained and unconstrained in #{c.name}"
389
- end
390
-
391
- puts \
392
- "#{subset_fact_types.map{|ft| ft.default_reading([], nil)}*" and "}" +
393
- "\n\t#{keyword "only if"} " +
394
- "#{superset_fact_types.map{|ft| ft.default_reading([], nil)}*" and "}" +
395
- ";"
396
- end
397
-
398
- end
399
- end
400
- end
401
- end
402
- end
403
-