intermine 0.98.01

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