intermine 0.98.01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1569 @@
1
+ require "rexml/document"
2
+ require "rexml/streamlistener"
3
+ require "stringio"
4
+ require "intermine/model"
5
+ require "intermine/results"
6
+ require "intermine/service"
7
+ require "intermine/lists"
8
+
9
+ include InterMine
10
+
11
+ unless String.instance_methods.include?(:start_with?)
12
+
13
+ class String
14
+
15
+ def start_with?(prefix)
16
+ prefix = Regexp.escape(prefix.to_s)
17
+ return self.match("^#{prefix}")
18
+ end
19
+
20
+ def end_with?(suffix)
21
+ suffix = Regexp.escape(suffix.to_s)
22
+ return self.match("#{suffix}$")
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ class Array
29
+ def every(count)
30
+ chunks = []
31
+ each_with_index do |item, index|
32
+ chunks << [] if index % count == 0
33
+ chunks.last << item
34
+ end
35
+ chunks
36
+ end
37
+ alias / every
38
+ end
39
+
40
+
41
+ module InterMine::PathQuery
42
+
43
+ include REXML
44
+
45
+ class QueryLoader
46
+
47
+ attr_reader :model
48
+
49
+ def initialize(model)
50
+ @model = model
51
+ end
52
+
53
+ def get_handler
54
+ return QueryBuilder.new(@model)
55
+ end
56
+
57
+ def parse(xml)
58
+ xml = StringIO.new(xml.to_s)
59
+ handler = get_handler
60
+ REXML::Document.parse_stream(xml, handler)
61
+ return handler.query
62
+ end
63
+
64
+ end
65
+
66
+ class TemplateLoader < QueryLoader
67
+
68
+ def get_handler
69
+ return TemplateBuilder.new(@model)
70
+ end
71
+ end
72
+
73
+ class QueryBuilder
74
+ include REXML::StreamListener
75
+
76
+ def initialize(model)
77
+ @model = model
78
+ @query_attributes = {}
79
+ @subclass_constraints = []
80
+ @coded_constraints = []
81
+ @joins = []
82
+ end
83
+
84
+ def query
85
+ q = create_query
86
+ # Add first, in case other bits depend on them
87
+ @subclass_constraints.each do |sc|
88
+ q.add_constraint(sc)
89
+ end
90
+ @joins.each do |j|
91
+ q.add_join(*j)
92
+ end
93
+ @coded_constraints.each do |con|
94
+ q.add_constraint(con)
95
+ end
96
+ @query_attributes.sort_by {|k, v| k}.reverse.each do |k,v|
97
+ begin
98
+ q.send(k + "=", v)
99
+ rescue
100
+ end
101
+ end
102
+ return q
103
+ end
104
+
105
+ def tag_start(name, attrs)
106
+ @in_value = false
107
+ if name == "query"
108
+ attrs.each do |a|
109
+ @query_attributes[a.first] = a.last if a.first != "model"
110
+ end
111
+ elsif name=="constraint"
112
+ process_constraint(attrs)
113
+ elsif name=="value"
114
+ @in_value = true
115
+ elsif name=="join"
116
+ @joins.push([attrs["path"], attrs["style"]])
117
+ end
118
+ end
119
+
120
+ def process_constraint(attrs)
121
+ if attrs.has_key?("type")
122
+ @subclass_constraints.push({:path => attrs["path"], :sub_class => attrs["type"]})
123
+ else
124
+ args = {}
125
+ args[:path] = attrs["path"]
126
+ args[:op] = attrs["op"]
127
+ args[:value] = attrs["value"] if attrs.has_key?("value")
128
+ args[:loopPath] = attrs["loopPath"] if attrs.has_key?("loopPath")
129
+ args[:extra_value] = attrs["extraValue"] if attrs.has_key?("extraValue")
130
+ args[:code] = attrs["code"]
131
+ if MultiValueConstraint.valid_ops.include?(attrs["op"])
132
+ args[:values] = [] # actual values will be pushed on later
133
+ end
134
+ if attrs.has_key?("loopPath")
135
+ LoopConstraint.xml_ops.each do |k,v|
136
+ args[:op] = k if v == args[:op]
137
+ end
138
+ end
139
+ @coded_constraints.push(args)
140
+ end
141
+ end
142
+
143
+ def text(t)
144
+ @coded_constraints.last[:values].push(t)
145
+ end
146
+
147
+ private
148
+
149
+ def create_query
150
+ return Query.new(@model)
151
+ end
152
+
153
+ end
154
+
155
+ class TemplateBuilder < QueryBuilder
156
+
157
+ def initialize(model)
158
+ super
159
+ @template_attrs = {}
160
+ end
161
+
162
+ def tag_start(name, attrs)
163
+ super
164
+ if name == "template"
165
+ attrs.each do |a|
166
+ @template_attrs[a.first] = a.last
167
+ end
168
+ end
169
+ end
170
+
171
+ def query
172
+ template = super
173
+ @template_attrs.each do |k,v|
174
+ template.send(k + '=', v)
175
+ end
176
+ return template
177
+ end
178
+
179
+ def process_constraint(attrs)
180
+ super
181
+ unless attrs.has_key? "type"
182
+ if attrs.has_key?("editable") and attrs["editable"].downcase == "false"
183
+ @coded_constraints.last[:editable] = false
184
+ else
185
+ @coded_constraints.last[:editable] = true
186
+ end
187
+ @coded_constraints.last[:switchable] = attrs["switchable"] || "locked"
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def create_query
194
+ return Template.new(@model, nil, @model.service)
195
+ end
196
+
197
+ end
198
+
199
+ # == A class representing a structured query against an InterMine Data-Warehouse
200
+ #
201
+ # Queries represent structured requests for data from an InterMine data-warehouse. They
202
+ # consist basically of output columns you select, and a set of constraints on the results
203
+ # to return. These are known as the "view" and the "constraints". In a nod to the SQL-origins
204
+ # of the queries, and to the syntax of ActiveRecord, there is both a method-chaining SQL-ish
205
+ # DSL, and a more isolating common InterMine DSL.
206
+ #
207
+ # query = service.query("Gene").select("*").where("proteins.molecularWeight" => {">" => 10000})
208
+ # query.each_result do |gene|
209
+ # puts gene.symbol
210
+ # end
211
+ #
212
+ # OR:
213
+ #
214
+ # query = service.query("Gene")
215
+ # query.add_views("*")
216
+ # query.add_constraint("proteins.molecularWeight", ">", 10000)
217
+ # ...
218
+ #
219
+ # The main differences from SQL are that the joining between tables is implicit
220
+ # and automatic. Simply by naming the column "Gene.proteins.molecularWeight" we have access
221
+ # to the protein table joined onto the gene table. (A consequence of this is that all queries must have a
222
+ # unique root that all paths descend from, and we do not permit right outer joins.)
223
+ #
224
+ # You can define the following features of a query:
225
+ # * The output column
226
+ # * The filtering constraints (what values certain columns must or must not have)
227
+ # * The sort order of the results
228
+ # * The way constraints are combined (AND or OR)
229
+ #
230
+ # In processing results, there are two powerful result formats available, depending on
231
+ # whether you want to process results row by row, or whether you would like the information grouped into
232
+ # logically coherent records. The latter is more similar to the ORM model, and can be seen above.
233
+ # The mechanisms we offer for row access allow accessing cell values of the result table
234
+ # transparently by index or column-name.
235
+ #
236
+ #:include:contact_header.rdoc
237
+ #
238
+ class Query
239
+
240
+ # The first possible constraint code
241
+ LOWEST_CODE = "A"
242
+
243
+ # The last possible constraint code
244
+ HIGHEST_CODE = "Z"
245
+
246
+ # The (optional) name of the query. Used in automatic access (eg: "query1")
247
+ attr_accessor :name
248
+
249
+ # A human readable title of the query (eg: "Gene --> Protein Domain")
250
+ attr_accessor :title
251
+
252
+ # The root class of the query.
253
+ attr_accessor :root
254
+
255
+ # The data model associated with the query
256
+ attr_reader :model
257
+
258
+ # All the current Join objects on the query
259
+ attr_reader :joins
260
+
261
+ # All the current constraints on the query
262
+ attr_reader :constraints
263
+
264
+ # All the columns currently selected for output.
265
+ attr_reader :views
266
+
267
+ # The current sort-order.
268
+ attr_reader :sort_order
269
+
270
+ # The current logic (as a LogicGroup)
271
+ attr_reader :logic
272
+
273
+ # The service this query is associated with
274
+ attr_reader :service
275
+
276
+ # URLs for internal consumption.
277
+ attr_reader :list_upload_uri, :list_append_uri
278
+
279
+ # Construct a new query object. You should not use this directly.
280
+ # Instead use the factory methods in Service.
281
+ #
282
+ # query = service.query("Gene")
283
+ #
284
+ def initialize(model, root=nil, service=nil)
285
+ @model = model
286
+ @service = service
287
+ @url = (@service.nil?) ? nil : @service.root + Service::QUERY_RESULTS_PATH
288
+ @list_upload_uri = (@service.nil?) ? nil : @service.root + Service::QUERY_TO_LIST_PATH
289
+ @list_append_uri = (@service.nil?) ? nil : @service.root + Service::QUERY_APPEND_PATH
290
+ if root
291
+ @root = InterMine::Metadata::Path.new(root, model).rootClass
292
+ end
293
+ @constraints = []
294
+ @joins = []
295
+ @views = []
296
+ @sort_order = []
297
+ @used_codes = []
298
+ @logic_parser = LogicParser.new(self)
299
+ @constraint_factory = ConstraintFactory.new(self)
300
+ end
301
+
302
+ # Return a parser for deserialising queries.
303
+ #
304
+ # parser = Query.parser(service.model)
305
+ # query = parser.parse(string)
306
+ # query.each_row |r|
307
+ # puts r.to_h
308
+ # end
309
+ #
310
+ def self.parser(model)
311
+ return QueryLoader.new(model)
312
+ end
313
+
314
+ # Return all the constraints that have codes and can thus
315
+ # participate in logic.
316
+ def coded_constraints
317
+ return @constraints.select {|x| !x.is_a?(SubClassConstraint)}
318
+ end
319
+
320
+ # Return all the constraints that restrict the class of
321
+ # paths in the query.
322
+ def subclass_constraints
323
+ return @constraints.select {|x| x.is_a?(SubClassConstraint)}
324
+ end
325
+
326
+ # Return an XML document node representing the XML form of the query.
327
+ #
328
+ # This is the canonical serialisable form of the query.
329
+ #
330
+ def to_xml
331
+ doc = REXML::Document.new
332
+
333
+ if @sort_order.empty?
334
+ so = SortOrder.new(@views.first, "ASC")
335
+ else
336
+ so = @sort_order.join(" ")
337
+ end
338
+
339
+ query = doc.add_element("query", {
340
+ "name" => @name,
341
+ "model" => @model.name,
342
+ "title" => @title,
343
+ "sortOrder" => so,
344
+ "view" => @views.join(" "),
345
+ "constraintLogic" => @logic
346
+ }.delete_if { |k, v | !v })
347
+ @joins.each { |join|
348
+ query.add_element("join", join.attrs)
349
+ }
350
+ subclass_constraints.each { |con|
351
+ query.add_element(con.to_elem)
352
+ }
353
+ coded_constraints.each { |con|
354
+ query.add_element(con.to_elem)
355
+ }
356
+ return doc
357
+ end
358
+
359
+ # Get your own result reader for handling the results at a low level.
360
+ def results_reader(start=0, size=nil)
361
+ return Results::ResultsReader.new(@url, self, start, size)
362
+ end
363
+
364
+ # Iterate over the results of this query one row at a time.
365
+ #
366
+ # Rows support both array-like index based access as well as
367
+ # hash-like key based access. For key based acces you can use
368
+ # either the full path or the headless short version:
369
+ #
370
+ # query.each_row do |row|
371
+ # puts r["Gene.symbol"], r["proteins.primaryIdentifier"]
372
+ # puts r[0]
373
+ # puts r.to_a # Materialize the row an an Array
374
+ # puts r.to_h # Materialize the row an a Hash
375
+ # end
376
+ #
377
+ def each_row(start=0, size=nil)
378
+ results_reader(start, size).each_row {|row|
379
+ yield row
380
+ }
381
+ end
382
+
383
+ # Iterate over the results, one record at a time.
384
+ #
385
+ # query.each_result do |gene|
386
+ # puts gene.symbol
387
+ # gene.proteins.each do |prot|
388
+ # puts prot.primaryIdentifier
389
+ # end
390
+ # end
391
+ #
392
+ def each_result(start=0, size=nil)
393
+ results_reader(start, size).each_result {|row|
394
+ yield row
395
+ }
396
+ end
397
+
398
+ # Return the number of result rows this query will return in its current state.
399
+ # This makes a very small request to the webservice, and is the most efficient
400
+ # method of getting the size of the result set.
401
+ def count
402
+ return results_reader.get_size
403
+ end
404
+
405
+ # Returns an Array of ResultRow objects containing the
406
+ # data returned by running this query, starting at the given offset and
407
+ # containing up to the given maximum size.
408
+ #
409
+ # The webservice enforces a maximum page-size of 10,000,000 rows,
410
+ # independent of any size you specify - this can be obviated with paging
411
+ # for large result sets.
412
+ #
413
+ # rows = query.rows
414
+ # rows.last["symbol"]
415
+ # => "eve"
416
+ #
417
+ def rows(start=0, size=nil)
418
+ res = []
419
+ results_reader(start, size).each_row {|row|
420
+ res << row
421
+ }
422
+ res
423
+ end
424
+
425
+ # Return objects corresponding to the type of data requested, starting
426
+ # at the given row offset.
427
+ #
428
+ # genes = query.results
429
+ # genes.last.symbol
430
+ # => "eve"
431
+ #
432
+ def results(start=0, size=nil)
433
+ res = []
434
+ results_reader(start, size).each_result {|row|
435
+ res << row
436
+ }
437
+ res
438
+ end
439
+
440
+ # Return all result record objects returned by running this query.
441
+ def all
442
+ return self.results
443
+ end
444
+
445
+ # Return all the rows returned by running the query
446
+ def all_rows
447
+ return self.rows
448
+ end
449
+
450
+ # Get the first result record from the query, starting at the
451
+ # given offset. If the offset is large, then this is not an efficient
452
+ # way to retrieve this data, and you may with to consider a looping approach
453
+ # or row based access instead.
454
+ def first(start=0)
455
+ current_row = 0
456
+ # Have to iterate as start refers to row count
457
+ results_reader.each_result { |r|
458
+ if current_row == start
459
+ return r
460
+ end
461
+ current_row += 1
462
+ }
463
+ return nil
464
+ end
465
+
466
+ # Get the first row of results from the query, starting at the given offset.
467
+ def first_row(start = 0)
468
+ return self.results(start, 1).first
469
+ end
470
+
471
+ # Get the constraint on the query with the given code.
472
+ # Raises an error if there is no such constraint.
473
+ def get_constraint(code)
474
+ @constraints.each do |x|
475
+ if x.respond_to?(:code) and x.code == code
476
+ return x
477
+ end
478
+ end
479
+ raise ArgumentError, "#{code} not in query"
480
+ end
481
+
482
+
483
+ # Remove the constraint with the given code from the query.
484
+ # If no such constraint exists, no error will be raised.
485
+ def remove_constraint(code)
486
+ @constraints.reject! do |x|
487
+ x.respond_to?(:code) and x.code == code
488
+ end
489
+ end
490
+
491
+ # Add the given views (output columns) to the query.
492
+ #
493
+ # Any columns ending in "*" will be interpreted as a request to add
494
+ # all attribute columns from that table to the query
495
+ #
496
+ # query = service.query("Gene")
497
+ # query.add_views("*")
498
+ # query.add_to_select("*")
499
+ # query.add_views("proteins.*")
500
+ # query.add_views("pathways.*", "organism.shortName")
501
+ #
502
+ def add_views(*views)
503
+ views.flatten.map do |x|
504
+ y = add_prefix(x)
505
+ if y.end_with?("*")
506
+ prefix = y.chomp(".*")
507
+ path = InterMine::Metadata::Path.new(prefix, @model, subclasses)
508
+ attrs = path.end_cd.attributes.map {|x| prefix + "." + x.name}
509
+ add_views(attrs)
510
+ else
511
+ path = InterMine::Metadata::Path.new(y, @model, subclasses)
512
+ if @root.nil?
513
+ @root = path.rootClass
514
+ end
515
+ @views << path
516
+ end
517
+ end
518
+ return self
519
+ end
520
+
521
+ alias add_to_select add_views
522
+
523
+ # Replace any currently existing views with the given view list.
524
+ # If the view is not already an Array, it will be split by commas and whitespace.
525
+ #
526
+ def view=(*view)
527
+ @views = []
528
+ view.each do |v|
529
+ if v.is_a?(Array)
530
+ views = v
531
+ else
532
+ views = v.to_s.split(/(?:,\s*|\s+)/)
533
+ end
534
+ add_views(*views)
535
+ end
536
+ return self
537
+ end
538
+
539
+ alias select view=
540
+
541
+ # Get the current sub-class map for this query.
542
+ #
543
+ # This contains information about which fields of this query have
544
+ # been declared to be restricted to contain only a subclass of their
545
+ # normal type.
546
+ #
547
+ # > query = service.query("Gene")
548
+ # > query.where(:microArrayResults => service.model.table("FlyAtlasResult"))
549
+ # > query.subclasses
550
+ # => {"Gene.microArrayResults" => "FlyAtlasResult"}
551
+ #
552
+ def subclasses
553
+ subclasses = {}
554
+ @constraints.each do |con|
555
+ if con.is_a?(SubClassConstraint)
556
+ subclasses[con.path.to_s] = con.sub_class.to_s
557
+ end
558
+ end
559
+ return subclasses
560
+ end
561
+
562
+ # Declare how a particular join should be treated.
563
+ #
564
+ # The default join style is for an INNER join, but joins can
565
+ # optionally be declared to be LEFT OUTER joins. The difference is
566
+ # that with an inner join, each join in the query implicitly constrains
567
+ # the values of that path to be non-null, whereas an outer-join allows
568
+ # null values in the joined path. If the path passed to the constructor
569
+ # has a chain of joins, the last section is the one the join is applied to.
570
+ #
571
+ # query = service.query("Gene")
572
+ # # Allow genes without proteins
573
+ # query.add_join("proteins")
574
+ # # Demand the results contain only those genes that have interactions that have interactingGenes,
575
+ # # but allow those interactingGenes to not have any proteins.
576
+ # query.add_join("interactions.interactingGenes.proteins")
577
+ #
578
+ # The valid join styles are OUTER and INNER (case-insensitive). There is never
579
+ # any need to declare a join to be INNER, as it is inner by default. Consider
580
+ # using Query#outerjoin which is more explicitly declarative.
581
+ #
582
+ def add_join(path, style="OUTER")
583
+ p = InterMine::Metadata::Path.new(add_prefix(path), @model, subclasses)
584
+ if @root.nil?
585
+ @root = p.rootClass
586
+ end
587
+ @joins << Join.new(p, style)
588
+ return self
589
+ end
590
+
591
+ alias join add_join
592
+
593
+ # Explicitly declare a join to be an outer join.
594
+ def outerjoin(path)
595
+ return add_join(path)
596
+ end
597
+
598
+ # Add a sort order element to sort order information.
599
+ # A sort order consists of the name of an output column and
600
+ # (optionally) the direction to sort in. The default direction is "ASC".
601
+ # The valid directions are "ASC" and "DESC" (case-insensitive).
602
+ #
603
+ # query.add_sort_order("length")
604
+ # query.add_sort_order("proteins.primaryIdentifier", "desc")
605
+ #
606
+ def add_sort_order(path, direction="ASC")
607
+ p = self.path(path)
608
+ if !@views.include? p
609
+ raise ArgumentError, "Sort order (#{p}) not in view (#{@views.map {|v| v.to_s}.inspect} in #{self.name || 'unnamed query'})"
610
+ end
611
+ @sort_order << SortOrder.new(p, direction)
612
+ return self
613
+ end
614
+
615
+ # Set the sort order completely, replacing the current sort order.
616
+ #
617
+ # query.sortOrder = "Gene.length asc Gene.proteins.length desc"
618
+ #
619
+ # The sort order expression will be parsed and checked for conformity with the
620
+ # current state of the query.
621
+ def sortOrder=(so)
622
+ if so.is_a?(Array)
623
+ sos = so
624
+ else
625
+ sos = so.split(/(ASC|DESC|asc|desc)/).map {|x| x.strip}.every(2)
626
+ end
627
+ sos.each do |args|
628
+ add_sort_order(*args)
629
+ end
630
+ end
631
+
632
+ alias order_by add_sort_order
633
+ alias order add_sort_order
634
+
635
+ # Add a constraint to the query matching the given parameters, and
636
+ # return the created constraint.
637
+ #
638
+ # con = query.add_constraint("length", ">", 500)
639
+ #
640
+ # Note that (at least for now) the style of argument used by where and
641
+ # add_constraint is not compatible. This is on the TODO list.
642
+ def add_constraint(*parameters)
643
+ con = @constraint_factory.make_constraint(parameters)
644
+ @constraints << con
645
+ return con
646
+ end
647
+
648
+ # Returns a Path object constructed from the given path-string,
649
+ # taking the current state of the query into account (its data-model
650
+ # and subclass constraints).
651
+ def path(pathstr)
652
+ return InterMine::Metadata::Path.new(add_prefix(pathstr), @model, subclasses)
653
+ end
654
+
655
+ # Add a constraint clause to the query.
656
+ #
657
+ # query.where(:symbol => "eve")
658
+ # query.where(:symbol => %{eve h bib zen})
659
+ # query.where(:length => {:le => 100}, :symbol => "eve*")
660
+ #
661
+ # Interprets the arguments in a style similar to that of
662
+ # ActiveRecord constraints, and adds them to the query.
663
+ # If multiple constraints are supplied in a single hash (as
664
+ # in the third example), then the order in which they are
665
+ # applied to the query (and thus the codes they will receive) is
666
+ # not predictable. To determine the order use chained where clauses
667
+ # or use multiple hashes:
668
+ #
669
+ # query.where({:length => {:le => 100}}, {:symbol => "eve*"})
670
+ #
671
+ # Returns self to support method chaining
672
+ #
673
+ def where(*wheres)
674
+ if @views.empty?
675
+ self.select('*')
676
+ end
677
+ wheres.each do |w|
678
+ w.each do |k,v|
679
+ if v.is_a?(Hash)
680
+ parameters = {:path => k}
681
+ v.each do |subk, subv|
682
+ normalised_k = subk.to_s.upcase.gsub(/_/, " ")
683
+ if subk == :with
684
+ parameters[:extra_value] = subv
685
+ elsif subk == :sub_class
686
+ parameters[subk] = subv
687
+ elsif subk == :code
688
+ parameters[:code] = subv
689
+ elsif LoopConstraint.valid_ops.include?(normalised_k)
690
+ parameters[:op] = normalised_k
691
+ parameters[:loopPath] = subv
692
+ else
693
+ if subv.nil?
694
+ if subk == "="
695
+ parameters[:op] = "IS NULL"
696
+ elsif subk == "!="
697
+ parameters[:op] = "IS NOT NULL"
698
+ else
699
+ parameters[:op] = normalised_k
700
+ end
701
+ elsif subv.is_a?(Range) or subv.is_a?(Array)
702
+ if subk == "="
703
+ parameters[:op] = "ONE OF"
704
+ elsif subk == "!="
705
+ parameters[:op] = "NONE OF"
706
+ else
707
+ parameters[:op] = normalised_k
708
+ end
709
+ parameters[:values] = subv.to_a
710
+ elsif subv.is_a?(Lists::List)
711
+ if subk == "="
712
+ parameters[:op] = "IN"
713
+ elsif subk == "!="
714
+ parameters[:op] = "NOT IN"
715
+ else
716
+ parameters[:op] = normalised_k
717
+ end
718
+ parameters[:value] = subv.name
719
+ else
720
+ parameters[:op] = normalised_k
721
+ parameters[:value] = subv
722
+ end
723
+ end
724
+ end
725
+ add_constraint(parameters)
726
+ elsif v.is_a?(Range) or v.is_a?(Array)
727
+ add_constraint(k.to_s, 'ONE OF', v.to_a)
728
+ elsif v.is_a?(InterMine::Metadata::ClassDescriptor)
729
+ add_constraint(:path => k.to_s, :sub_class => v.name)
730
+ elsif v.is_a?(InterMine::Lists::List)
731
+ add_constraint(k.to_s, 'IN', v.name)
732
+ elsif v.nil?
733
+ add_constraint(k.to_s, "IS NULL")
734
+ else
735
+ if path(k.to_s).is_attribute?
736
+ add_constraint(k.to_s, '=', v)
737
+ else
738
+ add_constraint(k.to_s, 'LOOKUP', v)
739
+ end
740
+ end
741
+ end
742
+ end
743
+ return self
744
+ end
745
+
746
+ # Set the logic to the given value.
747
+ #
748
+ # The value will be parsed for consistency is it is a logic
749
+ # string.
750
+ #
751
+ # Returns self to support chaining.
752
+ def set_logic(value)
753
+ if value.is_a?(LogicGroup)
754
+ @logic = value
755
+ else
756
+ @logic = @logic_parser.parse_logic(value)
757
+ end
758
+ return self
759
+ end
760
+
761
+ alias constraintLogic= set_logic
762
+
763
+ # Get the next available code for the query.
764
+ def next_code
765
+ c = LOWEST_CODE
766
+ while Query.is_valid_code(c)
767
+ return c unless used_codes.include?(c)
768
+ c = c.next
769
+ end
770
+ raise RuntimeError, "Maximum number of codes reached - all 26 have been allocated"
771
+ end
772
+
773
+ # Return the list of currently used codes by the query.
774
+ def used_codes
775
+ if @constraints.empty?
776
+ return []
777
+ else
778
+ return @constraints.select {|x| !x.is_a?(SubClassConstraint)}.map {|x| x.code}
779
+ end
780
+ end
781
+
782
+ # Whether or not the argument is a valid constraint code.
783
+ #
784
+ # to be valid, it must be a one character string between A and Z inclusive.
785
+ def self.is_valid_code(str)
786
+ return (str.length == 1) && (str >= LOWEST_CODE) && (str <= HIGHEST_CODE)
787
+ end
788
+
789
+ # Adds the root prefix to the given string.
790
+ #
791
+ # Arguments:
792
+ # [+x+] An object with a #to_s method
793
+ #
794
+ # Returns the prefixed string.
795
+ def add_prefix(x)
796
+ x = x.to_s
797
+ if @root && !x.start_with?(@root.name)
798
+ return @root.name + "." + x
799
+ else
800
+ return x
801
+ end
802
+ end
803
+
804
+ # Return the parameter hash for running this query in its current state.
805
+ def params
806
+ hash = {"query" => self.to_xml}
807
+ if @service and @service.token
808
+ hash["token"] = @service.token
809
+ end
810
+ return hash
811
+ end
812
+
813
+ # Return the textual representation of the query. Here it returns the Query XML
814
+ def to_s
815
+ return to_xml.to_s
816
+ end
817
+
818
+ # Return an informative textual representation of the query.
819
+ def inspect
820
+ return "<#{self.class.name} query=#{self.to_s.inspect}>"
821
+ end
822
+ end
823
+
824
+
825
+ class ConstraintFactory
826
+
827
+ def initialize(query)
828
+ @classes = [
829
+ SingleValueConstraint,
830
+ SubClassConstraint,
831
+ LookupConstraint, MultiValueConstraint,
832
+ UnaryConstraint, LoopConstraint, ListConstraint]
833
+
834
+ @query = query
835
+ end
836
+
837
+ def make_constraint(args)
838
+ case args.length
839
+ when 2
840
+ parameters = {:path => args[0], :op => args[1]}
841
+ when 3
842
+ if args[2].is_a?(Array)
843
+ parameters = {:path => args[0], :op => args[1], :values => args[2]}
844
+ elsif LoopConstraint.valid_ops.include?(args[1])
845
+ parameters = {:path => args[0], :op => args[1], :loopPath => args[2]}
846
+ else
847
+ parameters = {:path => args[0], :op => args[1], :value => args[2]}
848
+ end
849
+ when 4
850
+ parameters = {:path => args[0], :op => args[1], :value => args[2], :extra_value => args[3]}
851
+ else
852
+ parameters = args.first
853
+ end
854
+
855
+ attr_keys = parameters.keys
856
+ suitable_classes = @classes.select { |cls|
857
+ is_suitable = true
858
+ attr_keys.each { |key|
859
+ is_suitable = is_suitable && (cls.method_defined?(key))
860
+ if key.to_s == "op"
861
+ is_suitable = is_suitable && cls.valid_ops.include?(parameters[key])
862
+ end
863
+ }
864
+ is_suitable
865
+ }
866
+ if suitable_classes.size > 1
867
+ raise ArgumentError, "More than one class found for #{parameters.inspect}"
868
+ elsif suitable_classes.size < 1
869
+ raise ArgumentError, "No suitable classes found for #{parameters.inspect}"
870
+ end
871
+
872
+ cls = suitable_classes.first
873
+ con = cls.new
874
+ parameters.each_pair { |key, value|
875
+ if key == :path || key == :loopPath
876
+ value = @query.path(value)
877
+ end
878
+ if key == :sub_class
879
+ value = InterMine::Metadata::Path.new(value, @query.model)
880
+ end
881
+ con.send(key.to_s + '=', value)
882
+ }
883
+ con.validate
884
+ if con.respond_to?(:code)
885
+ code = con.code
886
+ if code.nil?
887
+ con.code = @query.next_code
888
+ else
889
+ code = code.to_s
890
+ unless Query.is_valid_code(code)
891
+ raise ArgumentError, "Coded must be between A and Z, got: #{code}"
892
+ end
893
+ if @query.used_codes.include?(code)
894
+ con.code = @query.next_code
895
+ end
896
+ end
897
+ end
898
+
899
+ return con
900
+ end
901
+
902
+
903
+ end
904
+
905
+ class TemplateConstraintFactory < ConstraintFactory
906
+
907
+ def initialize(query)
908
+ super
909
+ @classes = [
910
+ TemplateSingleValueConstraint,
911
+ SubClassConstraint,
912
+ TemplateLookupConstraint, TemplateMultiValueConstraint,
913
+ TemplateUnaryConstraint, TemplateLoopConstraint, TemplateListConstraint]
914
+ end
915
+
916
+ end
917
+
918
+ module PathFeature
919
+ attr_accessor :path
920
+
921
+ def validate
922
+ end
923
+ end
924
+
925
+ module TemplateConstraint
926
+
927
+ attr_accessor :editable, :switchable
928
+
929
+ def to_elem
930
+ attributes = {"editable" => @editable, "switchable" => @switchable}
931
+ elem = super
932
+ elem.add_attributes(attributes)
933
+ return elem
934
+ end
935
+
936
+ def template_param_op
937
+ return @op
938
+ end
939
+
940
+ end
941
+
942
+ module Coded
943
+ attr_accessor :code, :op
944
+
945
+ def self.valid_ops
946
+ return []
947
+ end
948
+
949
+ def to_elem
950
+ attributes = {
951
+ "path" => @path,
952
+ "op" => @op,
953
+ "code" => @code
954
+ }.delete_if {|k,v| !v}
955
+ elem = REXML::Element.new("constraint")
956
+ elem.add_attributes(attributes)
957
+ return elem
958
+ end
959
+ end
960
+
961
+ class SubClassConstraint
962
+ include PathFeature
963
+ attr_accessor :sub_class
964
+
965
+ def to_elem
966
+ attributes = {
967
+ "path" => @path,
968
+ "type" => @sub_class
969
+ }
970
+ elem = REXML::Element.new("constraint")
971
+ elem.add_attributes(attributes)
972
+ return elem
973
+ end
974
+
975
+ def validate
976
+ if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
977
+ raise ArgumentError, "#{self.class.name}s must be on objects or references to objects"
978
+ end
979
+ if @sub_class.length > 1
980
+ raise ArgumentError, "#{self.class.name} expects sub-classes to be named as bare class names"
981
+ end
982
+ model = @path.model
983
+ cdA = model.get_cd(@path.end_type)
984
+ cdB = model.get_cd(@sub_class.end_type)
985
+ unless ((cdB == cdA) or cdB.subclass_of?(cdA))
986
+ raise ArgumentError, "The subclass in a #{self.class.name} must be a subclass of its path, but #{cdB} is not a subclass of #{cdA}"
987
+ end
988
+
989
+ end
990
+
991
+ end
992
+
993
+ module ObjectConstraint
994
+ def validate
995
+ if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
996
+ raise ArgumentError, "#{self.class.name}s must be on objects or references to objects, got #{@path}"
997
+ end
998
+ end
999
+ end
1000
+
1001
+ module AttributeConstraint
1002
+ def validate
1003
+ if !@path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
1004
+ raise ArgumentError, "Attribute constraints must be on attributes, got #{@path}"
1005
+ end
1006
+ end
1007
+
1008
+ def coerce_value(val)
1009
+ nums = ["Float", "Double", "float", "double"]
1010
+ ints = ["Integer", "int"]
1011
+ bools = ["Boolean", "boolean"]
1012
+ dataType = @path.elements.last.dataType.split(".").last
1013
+ coerced = val
1014
+ if nums.include?(dataType)
1015
+ if !val.is_a?(Numeric)
1016
+ coerced = val.to_f
1017
+ end
1018
+ end
1019
+ if ints.include?(dataType)
1020
+ coerced = val.to_i
1021
+ end
1022
+ if bools.include?(dataType)
1023
+ if !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
1024
+ if val == 0 or val == "0" or val.downcase == "yes" or val.downcase == "true" or val.downcase == "t"
1025
+ coerced = true
1026
+ elsif val == 1 or val == "1" or val.downcase == "no" or val.downcase == "false" or val.downcase == "f"
1027
+ coerced = false
1028
+ end
1029
+ end
1030
+ end
1031
+ if coerced == 0 and not val.to_s.start_with?("0")
1032
+ raise ArgumentError, "cannot coerce #{val} to a #{dataType}"
1033
+ end
1034
+ return coerced
1035
+ end
1036
+
1037
+ def validate_value(val)
1038
+ nums = ["Float", "Double", "float", "double"]
1039
+ ints = ["Integer", "int"]
1040
+ bools = ["Boolean", "boolean"]
1041
+ dataType = @path.elements.last.dataType.split(".").last
1042
+ if nums.include?(dataType)
1043
+ if !val.is_a?(Numeric)
1044
+ raise ArgumentError, "value #{val} is not numeric for #{@path}"
1045
+ end
1046
+ end
1047
+ if ints.include?(dataType)
1048
+ val = val.to_i
1049
+ if !val.is_a?(Integer)
1050
+ raise ArgumentError, "value #{val} is not an integer for #{@path}"
1051
+ end
1052
+ end
1053
+ if bools.include?(dataType)
1054
+ if !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
1055
+ raise ArgumentError, "value #{val} is not a boolean value for #{@path}"
1056
+ end
1057
+ end
1058
+ end
1059
+ end
1060
+
1061
+ class SingleValueConstraint
1062
+ include PathFeature
1063
+ include Coded
1064
+ include AttributeConstraint
1065
+ attr_accessor :value
1066
+
1067
+ CANONICAL_OPS = {
1068
+ "EQ" => "=",
1069
+ "==" => "=",
1070
+ "NE" => "!=",
1071
+ "LT" => "<",
1072
+ "GT" => ">",
1073
+ "LE" => "<=",
1074
+ "GE" => ">="
1075
+ }
1076
+
1077
+ def self.valid_ops
1078
+ return %w{= == > < >= <= != CONTAINS LIKE EQ NE GT LT LE GE}
1079
+ end
1080
+
1081
+ def to_elem
1082
+ elem = super
1083
+ attributes = {"value" => @value}
1084
+ elem.add_attributes(attributes)
1085
+ return elem
1086
+ end
1087
+
1088
+ def validate
1089
+ super
1090
+ @op = SingleValueConstraint::CANONICAL_OPS[@op] || @op
1091
+ @value = coerce_value(@value)
1092
+ validate_value(@value)
1093
+ end
1094
+
1095
+ end
1096
+
1097
+ class TemplateSingleValueConstraint < SingleValueConstraint
1098
+ include TemplateConstraint
1099
+
1100
+ def template_param_op
1101
+ case @op
1102
+ when '='
1103
+ return 'eq'
1104
+ when '!='
1105
+ return 'ne'
1106
+ when '<'
1107
+ return 'lt'
1108
+ when '<='
1109
+ return 'le'
1110
+ when '>'
1111
+ return 'gt'
1112
+ when '>='
1113
+ return 'ge'
1114
+ else
1115
+ return @op
1116
+ end
1117
+ end
1118
+ end
1119
+
1120
+ class ListConstraint < SingleValueConstraint
1121
+ include ObjectConstraint
1122
+
1123
+ def self.valid_ops
1124
+ return ["IN", "NOT IN"]
1125
+ end
1126
+ end
1127
+
1128
+ class TemplateListConstraint < ListConstraint
1129
+ include TemplateConstraint
1130
+ end
1131
+
1132
+ class LoopConstraint
1133
+ include PathFeature
1134
+ include Coded
1135
+ attr_accessor :loopPath
1136
+
1137
+ def self.valid_ops
1138
+ return ["IS", "IS NOT"]
1139
+ end
1140
+
1141
+ def self.xml_ops
1142
+ return { "IS" => "=", "IS NOT" => "!=" }
1143
+ end
1144
+
1145
+ def to_elem
1146
+ elem = super
1147
+ elem.add_attribute("op", LoopConstraint.xml_ops[@op])
1148
+ elem.add_attribute("loopPath", @loopPath)
1149
+ return elem
1150
+ end
1151
+
1152
+ def validate
1153
+ if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
1154
+ raise ArgumentError, "#{self.class.name}s must be on objects or references to objects"
1155
+ end
1156
+ if @loopPath.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
1157
+ raise ArgumentError, "loopPaths on #{self.class.name}s must be on objects or references to objects"
1158
+ end
1159
+ model = @path.model
1160
+ cdA = model.get_cd(@path.end_type)
1161
+ cdB = model.get_cd(@loopPath.end_type)
1162
+ if !(cdA == cdB) && !cdA.subclass_of?(cdB) && !cdB.subclass_of?(cdA)
1163
+ raise ArgumentError, "Incompatible types in #{self.class.name}: #{@path} -> #{cdA} and #{@loopPath} -> #{cdB}"
1164
+ end
1165
+ end
1166
+
1167
+ end
1168
+
1169
+ class TemplateLoopConstraint < LoopConstraint
1170
+ include TemplateConstraint
1171
+ def template_param_op
1172
+ case @op
1173
+ when 'IS'
1174
+ return 'eq'
1175
+ when 'IS NOT'
1176
+ return 'ne'
1177
+ end
1178
+ end
1179
+ end
1180
+
1181
+ class UnaryConstraint
1182
+ include PathFeature
1183
+ include Coded
1184
+
1185
+ def self.valid_ops
1186
+ return ["IS NULL", "IS NOT NULL"]
1187
+ end
1188
+
1189
+ end
1190
+
1191
+ class TemplateUnaryConstraint < UnaryConstraint
1192
+ include TemplateConstraint
1193
+ end
1194
+
1195
+ class LookupConstraint < ListConstraint
1196
+ attr_accessor :extra_value
1197
+
1198
+ def self.valid_ops
1199
+ return ["LOOKUP"]
1200
+ end
1201
+
1202
+ def to_elem
1203
+ elem = super
1204
+ if @extra_value
1205
+ elem.add_attribute("extraValue", @extra_value)
1206
+ end
1207
+ return elem
1208
+ end
1209
+
1210
+ end
1211
+
1212
+ class TemplateLookupConstraint < LookupConstraint
1213
+ include TemplateConstraint
1214
+ end
1215
+
1216
+ class MultiValueConstraint
1217
+ include PathFeature
1218
+ include Coded
1219
+ include AttributeConstraint
1220
+
1221
+ def self.valid_ops
1222
+ return ["ONE OF", "NONE OF"]
1223
+ end
1224
+
1225
+ attr_accessor :values
1226
+ def to_elem
1227
+ elem = super
1228
+ @values.each { |x|
1229
+ value = REXML::Element.new("value")
1230
+ value.add_text(x.to_s)
1231
+ elem.add_element(value)
1232
+ }
1233
+ return elem
1234
+ end
1235
+
1236
+ def validate
1237
+ super
1238
+ @values.map! {|val| coerce_value(val)}
1239
+ @values.each do |val|
1240
+ validate_value(val)
1241
+ end
1242
+ end
1243
+ end
1244
+
1245
+ class TemplateMultiValueConstraint < MultiValueConstraint
1246
+ include TemplateConstraint
1247
+ end
1248
+
1249
+ class SortOrder
1250
+ include PathFeature
1251
+ attr_accessor :direction
1252
+ class << self; attr_accessor :valid_directions end
1253
+ @valid_directions = %w{ASC DESC}
1254
+
1255
+ def initialize(path, direction)
1256
+ direction = direction.to_s.upcase
1257
+ unless SortOrder.valid_directions.include? direction
1258
+ raise ArgumentError, "Illegal sort direction: #{direction}"
1259
+ end
1260
+ self.path = path
1261
+ self.direction = direction
1262
+ end
1263
+
1264
+ def to_s
1265
+ return @path.to_s + " " + @direction
1266
+ end
1267
+ end
1268
+
1269
+ class Join
1270
+ include PathFeature
1271
+ attr_accessor :style
1272
+ class << self; attr_accessor :valid_styles end
1273
+ @valid_styles = %{INNER OUTER}
1274
+
1275
+ def initialize(path, style)
1276
+ unless Join.valid_styles.include?(style)
1277
+ raise ArgumentError, "Invalid style: #{style}"
1278
+ end
1279
+ self.path = path
1280
+ self.style = style
1281
+ end
1282
+
1283
+ def attrs
1284
+ attributes = {
1285
+ "path" => @path,
1286
+ "style" => @style
1287
+ }
1288
+ return attributes
1289
+ end
1290
+ end
1291
+
1292
+ class LogicNode
1293
+ end
1294
+
1295
+ class LogicGroup < LogicNode
1296
+
1297
+ attr_reader :left, :right, :op
1298
+ attr_accessor :parent
1299
+
1300
+ def initialize(left, op, right, parent=nil)
1301
+ if !["AND", "OR"].include?(op)
1302
+ raise ArgumentError, "#{op} is not a legal logical operator"
1303
+ end
1304
+ @parent = parent
1305
+ @left = left
1306
+ @op = op
1307
+ @right = right
1308
+ [left, right].each do |node|
1309
+ if node.is_a?(LogicGroup)
1310
+ node.parent = self
1311
+ end
1312
+ end
1313
+ end
1314
+
1315
+ def to_s
1316
+ core = [@left.code, @op.downcase, @right.code].join(" ")
1317
+ if @parent && @op != @parent.op
1318
+ return "(#{core})"
1319
+ else
1320
+ return core
1321
+ end
1322
+ end
1323
+
1324
+ def code
1325
+ return to_s
1326
+ end
1327
+
1328
+ end
1329
+
1330
+ class LogicParseError < ArgumentError
1331
+ end
1332
+
1333
+ class LogicParser
1334
+
1335
+ class << self; attr_accessor :precedence, :ops end
1336
+ @precedence = {
1337
+ "AND" => 2,
1338
+ "OR" => 1,
1339
+ "(" => 3,
1340
+ ")" => 3
1341
+ }
1342
+
1343
+ @ops = {
1344
+ "AND" => "AND",
1345
+ "&" => "AND",
1346
+ "&&" => "AND",
1347
+ "OR" => "OR",
1348
+ "|" => "OR",
1349
+ "||" => "OR",
1350
+ "(" => "(",
1351
+ ")" => ")"
1352
+ }
1353
+
1354
+ def initialize(query)
1355
+ @query = query
1356
+ end
1357
+
1358
+ def parse_logic(str)
1359
+ tokens = str.upcase.split(/(?:\s+|\b)/).map do |x|
1360
+ LogicParser.ops.fetch(x, x.split(//))
1361
+ end
1362
+ tokens.flatten!
1363
+
1364
+ check_syntax(tokens)
1365
+ postfix_tokens = infix_to_postfix(tokens)
1366
+ ast = postfix_to_tree(postfix_tokens)
1367
+ return ast
1368
+ end
1369
+
1370
+ private
1371
+
1372
+ def infix_to_postfix(tokens)
1373
+ stack = []
1374
+ postfix_tokens = []
1375
+ tokens.each do |x|
1376
+ if !LogicParser.ops.include?(x)
1377
+ postfix_tokens << x
1378
+ else
1379
+ case x
1380
+ when "("
1381
+ stack << x
1382
+ when ")"
1383
+ while !stack.empty?
1384
+ last_op = stack.pop
1385
+ if last_op == "("
1386
+ if !stack.empty?
1387
+ previous_op = stack.pop
1388
+ if previous_op != "("
1389
+ postfix_tokens << previous_op
1390
+ break
1391
+ end
1392
+ end
1393
+ else
1394
+ postfix_tokens << last_op
1395
+ end
1396
+ end
1397
+ else
1398
+ while !stack.empty? and LogicParser.precedence[stack.last] <= LogicParser.precedence[x]
1399
+ prev_op = stack.pop
1400
+ if prev_op != "("
1401
+ postfix_tokens << prev_op
1402
+ end
1403
+ end
1404
+ stack << x
1405
+ end
1406
+ end
1407
+ end
1408
+ while !stack.empty?
1409
+ postfix_tokens << stack.pop
1410
+ end
1411
+ return postfix_tokens
1412
+ end
1413
+
1414
+ def check_syntax(tokens)
1415
+ need_op = false
1416
+ need_bin_op_or_bracket = false
1417
+ processed = []
1418
+ open_brackets = 0
1419
+ tokens.each do |x|
1420
+ if !LogicParser.ops.include?(x)
1421
+ if need_op
1422
+ raise LogicParseError, "Expected an operator after '#{processed.join(' ')}', but got #{x}"
1423
+ elsif need_bin_op_or_bracket
1424
+ raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', expected an operator or closing bracket, but got #{x}"
1425
+ end
1426
+ need_op = true
1427
+ else
1428
+ need_op = false
1429
+ case x
1430
+ when "("
1431
+ if !processed.empty? && !LogicParser.ops.include?(processed.last)
1432
+ raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', got #{x}"
1433
+ elsif need_bin_op_or_bracket
1434
+ raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', got #{x}"
1435
+ end
1436
+ open_brackets += 1
1437
+ when ")"
1438
+ need_bin_op_or_bracket = true
1439
+ open_brackets -= 1
1440
+ else
1441
+ need_bin_op_or_bracket = false
1442
+ end
1443
+ end
1444
+ processed << x
1445
+ end
1446
+ if open_brackets < 0
1447
+ raise LogicParseError, "Unmatched closing bracket in #{tokens.join(' ')}"
1448
+ elsif open_brackets > 0
1449
+ raise LogicParseError, "Unmatched opening bracket in #{tokens.join(' ')}"
1450
+ end
1451
+ end
1452
+
1453
+ def postfix_to_tree(tokens)
1454
+ stack = []
1455
+ tokens.each do |x|
1456
+ if !LogicParser.ops.include?(x)
1457
+ stack << x
1458
+ else
1459
+ right = stack.pop
1460
+ left = stack.pop
1461
+ right = (right.is_a?(LogicGroup)) ? right : @query.get_constraint(right)
1462
+ left = (left.is_a?(LogicGroup)) ? left : @query.get_constraint(left)
1463
+ stack << LogicGroup.new(left, x, right)
1464
+ end
1465
+ end
1466
+ if stack.size != 1
1467
+ raise LogicParseError, "Tree does not have a unique root"
1468
+ end
1469
+ return stack.pop
1470
+ end
1471
+
1472
+ def precedence_of(op)
1473
+ return LogicParser.precedence[op]
1474
+ end
1475
+
1476
+ end
1477
+
1478
+ class Template < Query
1479
+
1480
+ attr_accessor :longDescription, :comment
1481
+
1482
+ def initialize(model, root=nil, service=nil)
1483
+ super
1484
+ @constraint_factory = TemplateConstraintFactory.new(self)
1485
+ @url = (@service.nil?) ? nil : @service.root + Service::TEMPLATE_RESULTS_PATH
1486
+ end
1487
+
1488
+ def self.parser(model)
1489
+ return TemplateLoader.new(model)
1490
+ end
1491
+
1492
+ def to_xml
1493
+ doc = REXML::Document.new
1494
+ t = doc.add_element 'template', {"name" => @name, "title" => @title, "longDescription" => @longDescription, "comment" => @comment}.reject {|k,v| v.nil?}
1495
+ t.add_element super
1496
+ return t
1497
+ end
1498
+
1499
+ def editable_constraints
1500
+ return coded_constraints.select {|con| con.editable}
1501
+ end
1502
+
1503
+ def active_constraints
1504
+ return coded_constraints.select {|con| con.switchable != "off"}
1505
+ end
1506
+
1507
+ def params
1508
+ p = {"name" => @name}
1509
+ actives = active_constraints
1510
+ actives.each_index do |idx|
1511
+ con = actives[idx]
1512
+ count = (idx + 1).to_s
1513
+ p["constraint" + count] = con.path.to_s
1514
+ p["op" + count] = con.template_param_op
1515
+ if con.respond_to? :value
1516
+ p["value" + count] = con.value
1517
+ elsif con.respond_to? :values
1518
+ p["value" + count] = con.values
1519
+ elsif con.respond_to? :loopPath
1520
+ p["loopPath" + count] = con.loopPath.to_s
1521
+ end
1522
+ if con.respond_to? :extra_value and !con.extra_value.nil?
1523
+ p["extra" + count] = con.extra_value
1524
+ end
1525
+ end
1526
+ return p
1527
+ end
1528
+
1529
+ def each_row(params = {}, start=0, size=nil)
1530
+ runner = (params.empty?) ? self : get_adjusted(params)
1531
+ runner.results_reader(start, size).each_row {|r| yield r}
1532
+ end
1533
+
1534
+ def each_result(params = {}, start=0, size=nil)
1535
+ runner = (params.empty?) ? self : get_adjusted(params)
1536
+ runner.results_reader(start, size).each_result {|r| yield r}
1537
+ end
1538
+
1539
+ def count(params = {})
1540
+ runner = (params.empty?) ? self : get_adjusted(params)
1541
+ runner.results_reader.get_size
1542
+ end
1543
+
1544
+ def clone
1545
+ other = super
1546
+ other.instance_variable_set(:@constraints, @constraints.map {|c| c.clone})
1547
+ return other
1548
+ end
1549
+
1550
+ private
1551
+
1552
+ def get_adjusted(params)
1553
+ adjusted = clone
1554
+ params.each do |k,v|
1555
+ con = adjusted.get_constraint(k)
1556
+ raise ArgumentError, "There is no constraint with code #{k} in this query" unless con
1557
+ path = con.path.to_s
1558
+ adjusted.remove_constraint(k)
1559
+ adjusted.where(path => v)
1560
+ adjusted.constraints.last.code = k
1561
+ end
1562
+ return adjusted
1563
+ end
1564
+ end
1565
+
1566
+ class TemplateConstraintFactory < ConstraintFactory
1567
+ end
1568
+
1569
+ end