]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
090b944f7cb08fd8ef48bdb6773306b8b32e3683
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / QueryParser.pm
1 use strict;
2 use warnings;
3
4 package QueryParser;
5 use OpenSRF::Utils::JSON;
6 use Data::Dumper;
7
8 =head1 NAME
9
10 QueryParser - basic QueryParser class
11
12 =head1 SYNOPSIS
13
14 use QueryParser;
15 my $QParser = QueryParser->new(%args);
16
17 =head1 DESCRIPTION
18
19 Main entrypoint into the QueryParser functionality.
20
21 =head1 FUNCTIONS
22
23 =cut
24
25 # Note that the first key must match the name of the package.
26 our %parser_config = (
27     QueryParser => {
28         filters => [],
29         modifiers => [],
30         operators => { 
31             'and' => '&&',
32             'or' => '||',
33             float_start => '{{',
34             float_end => '}}',
35             group_start => '(',
36             group_end => ')',
37             required => '+',
38             disallowed => '-',
39             modifier => '#',
40             negated => '!'
41         }
42     }
43 );
44
45 sub canonicalize {
46     my $self = shift;
47     return QueryParser::Canonicalize::abstract_query2str_impl(
48         $self->parse_tree->to_abstract_query(@_)
49     );
50 }
51
52
53 =head2 facet_class_count
54
55     $count = $QParser->facet_class_count();
56 =cut
57
58 sub facet_class_count {
59     my $self = shift;
60     return @{$self->facet_classes};
61 }
62
63 =head2 search_class_count
64
65     $count = $QParser->search_class_count();
66 =cut
67
68 sub search_class_count {
69     my $self = shift;
70     return @{$self->search_classes};
71 }
72
73 =head2 filter_count
74
75     $count = $QParser->filter_count();
76 =cut
77
78 sub filter_count {
79     my $self = shift;
80     return @{$self->filters};
81 }
82
83 =head2 modifier_count
84
85     $count = $QParser->modifier_count();
86 =cut
87
88 sub modifier_count {
89     my $self = shift;
90     return @{$self->modifiers};
91 }
92
93 =head2 custom_data
94
95     $data = $QParser->custom_data($class);
96 =cut
97
98 sub custom_data {
99     my $class = shift;
100     $class = ref($class) || $class;
101
102     $parser_config{$class}{custom_data} ||= {};
103     return $parser_config{$class}{custom_data};
104 }
105
106 =head2 operators
107
108     $operators = $QParser->operators();
109
110 Returns hashref of the configured operators.
111 =cut
112
113 sub operators {
114     my $class = shift;
115     $class = ref($class) || $class;
116
117     $parser_config{$class}{operators} ||= {};
118     return $parser_config{$class}{operators};
119 }
120
121 sub allow_nested_modifiers {
122     my $class = shift;
123     my $v = shift;
124     $class = ref($class) || $class;
125
126     $parser_config{$class}{allow_nested_modifiers} = $v if (defined $v);
127     return $parser_config{$class}{allow_nested_modifiers};
128 }
129
130 =head2 filters
131
132     $filters = $QParser->filters();
133
134 Returns arrayref of the configured filters.
135 =cut
136
137 sub filters {
138     my $class = shift;
139     $class = ref($class) || $class;
140
141     $parser_config{$class}{filters} ||= [];
142     return $parser_config{$class}{filters};
143 }
144
145 =head2 filter_callbacks
146
147     $filter_callbacks = $QParser->filter_callbacks();
148
149 Returns hashref of the configured filter callbacks.
150 =cut
151
152 sub filter_callbacks {
153     my $class = shift;
154     $class = ref($class) || $class;
155
156     $parser_config{$class}{filter_callbacks} ||= {};
157     return $parser_config{$class}{filter_callbacks};
158 }
159
160 =head2 modifiers
161
162     $modifiers = $QParser->modifiers();
163
164 Returns arrayref of the configured modifiers.
165 =cut
166
167 sub modifiers {
168     my $class = shift;
169     $class = ref($class) || $class;
170
171     $parser_config{$class}{modifiers} ||= [];
172     return $parser_config{$class}{modifiers};
173 }
174
175 =head2 new
176
177     $QParser = QueryParser->new(%args);
178
179 Creates a new QueryParser object.
180 =cut
181
182 sub new {
183     my $class = shift;
184     $class = ref($class) || $class;
185
186     my %opts = @_;
187
188     my $self = bless {} => $class;
189
190     for my $o (keys %{QueryParser->operators}) {
191         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
192     }
193
194     for my $opt ( keys %opts) {
195         $self->$opt( $opts{$opt} ) if ($self->can($opt));
196     }
197
198     return $self;
199 }
200
201 =head2 new_plan
202
203     $query_plan = $QParser->new_plan();
204
205 Create a new query plan.
206 =cut
207
208 sub new_plan {
209     my $self = shift;
210     my $pkg = ref($self) || $self;
211     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
212 }
213
214 =head2 add_search_filter
215
216     $QParser->add_search_filter($filter, [$callback]);
217
218 Adds a filter with the specified name and an optional callback to the
219 QueryParser configuration.
220 =cut
221
222 sub add_search_filter {
223     my $pkg = shift;
224     $pkg = ref($pkg) || $pkg;
225     my $filter = shift;
226     my $callback = shift;
227
228     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
229     push @{$pkg->filters}, $filter;
230     $pkg->filter_callbacks->{$filter} = $callback if ($callback);
231     return $filter;
232 }
233
234 =head2 add_search_modifier
235
236     $QParser->add_search_modifier($modifier);
237
238 Adds a modifier with the specified name to the QueryParser configuration.
239 =cut
240
241 sub add_search_modifier {
242     my $pkg = shift;
243     $pkg = ref($pkg) || $pkg;
244     my $modifier = shift;
245
246     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
247     push @{$pkg->modifiers}, $modifier;
248     return $modifier;
249 }
250
251 =head2 add_facet_class
252
253     $QParser->add_facet_class($facet_class);
254
255 Adds a facet class with the specified name to the QueryParser configuration.
256 =cut
257
258 sub add_facet_class {
259     my $pkg = shift;
260     $pkg = ref($pkg) || $pkg;
261     my $class = shift;
262
263     return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
264
265     push @{$pkg->facet_classes}, $class;
266     $pkg->facet_fields->{$class} = [];
267
268     return $class;
269 }
270
271 =head2 add_search_class
272
273     $QParser->add_search_class($class);
274
275 Adds a search class with the specified name to the QueryParser configuration.
276 =cut
277
278 sub add_search_class {
279     my $pkg = shift;
280     $pkg = ref($pkg) || $pkg;
281     my $class = shift;
282
283     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
284
285     push @{$pkg->search_classes}, $class;
286     $pkg->search_fields->{$class} = [];
287     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
288
289     return $class;
290 }
291
292 =head2 add_search_modifier
293
294     $op = $QParser->operator($operator, [$newvalue]);
295
296 Retrieves or sets value for the specified operator. Valid operators and
297 their defaults are as follows:
298
299 =over 4
300
301 =item * and => &&
302
303 =item * or => ||
304
305 =item * group_start => (
306
307 =item * group_end => )
308
309 =item * required => +
310
311 =item * disallowed => -
312
313 =item * modifier => #
314
315 =back
316
317 =cut
318
319 sub operator {
320     my $class = shift;
321     $class = ref($class) || $class;
322     my $opname = shift;
323     my $op = shift;
324
325     return undef unless ($opname);
326
327     $parser_config{$class}{operators} ||= {};
328     $parser_config{$class}{operators}{$opname} = $op if ($op);
329
330     return $parser_config{$class}{operators}{$opname};
331 }
332
333 =head2 facet_classes
334
335     $classes = $QParser->facet_classes([\@newclasses]);
336
337 Returns arrayref of all configured facet classes after optionally
338 replacing configuration.
339 =cut
340
341 sub facet_classes {
342     my $class = shift;
343     $class = ref($class) || $class;
344     my $classes = shift;
345
346     $parser_config{$class}{facet_classes} ||= [];
347     $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
348     return $parser_config{$class}{facet_classes};
349 }
350
351 =head2 search_classes
352
353     $classes = $QParser->search_classes([\@newclasses]);
354
355 Returns arrayref of all configured search classes after optionally
356 replacing the previous configuration.
357 =cut
358
359 sub search_classes {
360     my $class = shift;
361     $class = ref($class) || $class;
362     my $classes = shift;
363
364     $parser_config{$class}{classes} ||= [];
365     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
366     return $parser_config{$class}{classes};
367 }
368
369 =head2 add_query_normalizer
370
371     $function = $QParser->add_query_normalizer($class, $field, $func, [\@params]);
372
373 =cut
374
375 sub add_query_normalizer {
376     my $pkg = shift;
377     $pkg = ref($pkg) || $pkg;
378     my $class = shift;
379     my $field = shift;
380     my $func = shift;
381     my $params = shift || [];
382
383     # do not add if function AND params are identical to existing member
384     return $func if (grep {
385         $_->{function} eq $func and 
386         OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
387     } @{$pkg->query_normalizers->{$class}->{$field}});
388
389     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
390
391     return $func;
392 }
393
394 =head2 query_normalizers
395
396     $normalizers = $QParser->query_normalizers($class, $field);
397
398 Returns a list of normalizers associated with the specified search class
399 and field
400 =cut
401
402 sub query_normalizers {
403     my $pkg = shift;
404     $pkg = ref($pkg) || $pkg;
405
406     my $class = shift;
407     my $field = shift;
408
409     $parser_config{$pkg}{normalizers} ||= {};
410     if ($class) {
411         if ($field) {
412             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
413             return $parser_config{$pkg}{normalizers}{$class}{$field};
414         } else {
415             return $parser_config{$pkg}{normalizers}{$class};
416         }
417     }
418
419     return $parser_config{$pkg}{normalizers};
420 }
421
422 =head2 add_filter_normalizer
423
424     $normalizer = $QParser->add_filter_normalizer($filter, $func, [\@params]);
425
426 Adds a normalizer function to the specified filter.
427 =cut
428
429 sub add_filter_normalizer {
430     my $pkg = shift;
431     $pkg = ref($pkg) || $pkg;
432     my $filter = shift;
433     my $func = shift;
434     my $params = shift || [];
435
436     return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
437
438     push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
439
440     return $func;
441 }
442
443 =head2 filter_normalizers
444
445     $normalizers = $QParser->filter_normalizers($filter);
446
447 Return arrayref of normalizer functions associated with the specified filter.
448 =cut
449
450 sub filter_normalizers {
451     my $pkg = shift;
452     $pkg = ref($pkg) || $pkg;
453
454     my $filter = shift;
455
456     $parser_config{$pkg}{filter_normalizers} ||= {};
457     if ($filter) {
458         $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
459         return $parser_config{$pkg}{filter_normalizers}{$filter};
460     }
461
462     return $parser_config{$pkg}{filter_normalizers};
463 }
464
465 =head2 default_search_class
466
467     $default_class = $QParser->default_search_class([$class]);
468
469 Set or return the default search class.
470 =cut
471
472 sub default_search_class {
473     my $pkg = shift;
474     $pkg = ref($pkg) || $pkg;
475     my $class = shift;
476     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
477
478     return $QueryParser::parser_config{$pkg}{default_class};
479 }
480
481 =head2 remove_facet_class
482
483     $QParser->remove_facet_class($class);
484
485 Remove the specified facet class from the configuration.
486 =cut
487
488 sub remove_facet_class {
489     my $pkg = shift;
490     $pkg = ref($pkg) || $pkg;
491     my $class = shift;
492
493     return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
494
495     $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
496     delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
497
498     return $class;
499 }
500
501 =head2 remove_search_class
502
503     $QParser->remove_search_class($class);
504
505 Remove the specified search class from the configuration.
506 =cut
507
508 sub remove_search_class {
509     my $pkg = shift;
510     $pkg = ref($pkg) || $pkg;
511     my $class = shift;
512
513     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
514
515     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
516     delete $QueryParser::parser_config{$pkg}{fields}{$class};
517
518     return $class;
519 }
520
521 =head2 add_facet_field
522
523     $QParser->add_facet_field($class, $field);
524
525 Adds the specified field (and facet class if it doesn't already exist)
526 to the configuration.
527 =cut
528
529 sub add_facet_field {
530     my $pkg = shift;
531     $pkg = ref($pkg) || $pkg;
532     my $class = shift;
533     my $field = shift;
534
535     $pkg->add_facet_class( $class );
536
537     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
538
539     push @{$pkg->facet_fields->{$class}}, $field;
540
541     return { $class => $field };
542 }
543
544 =head2 facet_fields
545
546     $fields = $QParser->facet_fields($class);
547
548 Returns arrayref with list of fields for specified facet class.
549 =cut
550
551 sub facet_fields {
552     my $class = shift;
553     $class = ref($class) || $class;
554
555     $parser_config{$class}{facet_fields} ||= {};
556     return $parser_config{$class}{facet_fields};
557 }
558
559 =head2 add_search_field
560
561     $QParser->add_search_field($class, $field);
562
563 Adds the specified field (and facet class if it doesn't already exist)
564 to the configuration.
565 =cut
566
567 sub add_search_field {
568     my $pkg = shift;
569     $pkg = ref($pkg) || $pkg;
570     my $class = shift;
571     my $field = shift;
572
573     $pkg->add_search_class( $class );
574
575     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
576
577     push @{$pkg->search_fields->{$class}}, $field;
578
579     return { $class => $field };
580 }
581
582 =head2 search_fields
583
584     $fields = $QParser->search_fields();
585
586 Returns arrayref with list of configured search fields.
587 =cut
588
589 sub search_fields {
590     my $class = shift;
591     $class = ref($class) || $class;
592
593     $parser_config{$class}{fields} ||= {};
594     return $parser_config{$class}{fields};
595 }
596
597 =head2 add_search_class_alias
598
599     $QParser->add_search_class_alias($class, $alias);
600 =cut
601
602 sub add_search_class_alias {
603     my $pkg = shift;
604     $pkg = ref($pkg) || $pkg;
605     my $class = shift;
606     my $alias = shift;
607
608     $pkg->add_search_class( $class );
609
610     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
611
612     push @{$pkg->search_class_aliases->{$class}}, $alias;
613
614     return { $class => $alias };
615 }
616
617 =head2 search_class_aliases
618
619     $aliases = $QParser->search_class_aliases($class);
620 =cut
621
622 sub search_class_aliases {
623     my $class = shift;
624     $class = ref($class) || $class;
625
626     $parser_config{$class}{class_map} ||= {};
627     return $parser_config{$class}{class_map};
628 }
629
630 =head2 add_search_field_alias
631
632     $QParser->add_search_field_alias($class, $field, $alias);
633 =cut
634
635 sub add_search_field_alias {
636     my $pkg = shift;
637     $pkg = ref($pkg) || $pkg;
638     my $class = shift;
639     my $field = shift;
640     my $alias = shift;
641
642     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
643
644     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
645
646     return { $class => { $field => $alias } };
647 }
648
649 =head2 search_field_aliases
650
651     $aliases = $QParser->search_field_aliases();
652 =cut
653
654 sub search_field_aliases {
655     my $class = shift;
656     $class = ref($class) || $class;
657
658     $parser_config{$class}{field_alias_map} ||= {};
659     return $parser_config{$class}{field_alias_map};
660 }
661
662 =head2 remove_facet_field
663
664     $QParser->remove_facet_field($class, $field);
665 =cut
666
667 sub remove_facet_field {
668     my $pkg = shift;
669     $pkg = ref($pkg) || $pkg;
670     my $class = shift;
671     my $field = shift;
672
673     return { $class => $field }  if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
674
675     $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
676
677     return { $class => $field };
678 }
679
680 =head2 remove_search_field
681
682     $QParser->remove_search_field($class, $field);
683 =cut
684
685 sub remove_search_field {
686     my $pkg = shift;
687     $pkg = ref($pkg) || $pkg;
688     my $class = shift;
689     my $field = shift;
690
691     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
692
693     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
694
695     return { $class => $field };
696 }
697
698 =head2 remove_search_field_alias
699
700     $QParser->remove_search_field_alias($class, $field, $alias);
701 =cut
702
703 sub remove_search_field_alias {
704     my $pkg = shift;
705     $pkg = ref($pkg) || $pkg;
706     my $class = shift;
707     my $field = shift;
708     my $alias = shift;
709
710     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
711
712     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
713
714     return { $class => { $field => $alias } };
715 }
716
717 =head2 remove_search_class_alias
718
719     $QParser->remove_search_class_alias($class, $alias);
720 =cut
721
722 sub remove_search_class_alias {
723     my $pkg = shift;
724     $pkg = ref($pkg) || $pkg;
725     my $class = shift;
726     my $alias = shift;
727
728     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
729
730     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
731
732     return { $class => $alias };
733 }
734
735 =head2 debug
736
737     $debug = $QParser->debug([$debug]);
738
739 Return or set whether debugging output is enabled.
740 =cut
741
742 sub debug {
743     my $self = shift;
744     my $q = shift;
745     $self->{_debug} = $q if (defined $q);
746     return $self->{_debug};
747 }
748
749 =head2 query
750
751     $query = $QParser->query([$query]);
752
753 Return or set the query.
754 =cut
755
756 sub query {
757     my $self = shift;
758     my $q = shift;
759     $self->{_query} = " $q " if (defined $q);
760     return $self->{_query};
761 }
762
763 =head2 parse_tree
764
765     $parse_tree = $QParser->parse_tree([$parse_tree]);
766
767 Return or set the parse tree associated with the QueryParser.
768 =cut
769
770 sub parse_tree {
771     my $self = shift;
772     my $q = shift;
773     $self->{_parse_tree} = $q if (defined $q);
774     return $self->{_parse_tree};
775 }
776
777 sub floating_plan {
778     my $self = shift;
779     my $q = shift;
780     $self->{_top} = $q if (defined $q);
781     return $self->{_top};
782 }
783
784 =head2 parse
785
786     $QParser->parse([$query]);
787
788 Parse the specified query, or the query already associated with the QueryParser
789 object.
790 =cut
791
792 our $last_type = '';
793 our $last_class = '';
794 our $floating = 0;
795 our $fstart;
796 sub parse {
797     my $self = shift;
798     my $pkg = ref($self) || $self;
799     warn " ** parse package is $pkg\n" if $self->debug;
800
801     # Reset at each top-level parsing request
802     $last_class = '';
803     $last_type = '';
804     $floating = 0;
805     $fstart = undef;
806
807     $self->decompose( $self->query( shift() ) );
808
809     if ($self->floating_plan) {
810         $self->floating_plan->add_node( $self->parse_tree );
811         $self->parse_tree( $self->floating_plan );
812     }
813
814     warn "Query tree before pullup:\n" . Dumper($self->parse_tree) if $self->debug;
815     $self->parse_tree( $self->parse_tree->pullup );
816     $self->parse_tree->plan_level(0);
817
818     return $self;
819 }
820
821 =head2 decompose
822
823     ($struct, $remainder) = $QParser->decompose($querystring, [$current_class], [$recursing], [$phrase_helper]);
824
825 This routine does the heavy work of parsing the query string recursively.
826 Returns the top level query plan, or the query plan from a lower level plus
827 the portion of the query string that needs to be processed at a higher level.
828 =cut
829
830 our $_compiled_decomposer = {};
831 sub decompose {
832     my $self = shift;
833     my $pkg = ref($self) || $self;
834
835     my $r = $$_compiled_decomposer{$pkg};
836     my $compiled = defined($r);
837
838     $_ = shift;
839     my $current_class = shift || $self->default_search_class;
840
841     my $recursing = shift || 0;
842     my $phrase_helper = shift || 0;
843
844     warn '  'x$recursing." ** QP: decompose package is $pkg" if $self->debug;
845
846     if (!$compiled) {
847         $r = $$_compiled_decomposer{$pkg} = {};
848         warn '  'x$recursing." ** Compiling decomposer\n" if $self->debug;
849
850         # Build the search class+field uber-regexp
851         $$r{search_class_re} = '^\s*(';
852     } else {
853         warn '  'x$recursing." ** Decomposer already compiled\n" if $self->debug;
854     }
855
856     my $first_class = 1;
857
858     my %seen_classes;
859     for my $class ( keys %{$pkg->search_field_aliases} ) {
860         warn '  'x$recursing." *** ... Looking for search fields in $class\n" if $self->debug;
861
862         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
863             warn '  'x$recursing." *** ... Looking for aliases of $field\n" if $self->debug;
864
865             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
866                 my $aliasr = qr/$alias/;
867                 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
868                 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
869                 warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
870             }
871         }
872
873         if (!$compiled) {
874             $$r{search_class_re} .= '|' unless ($first_class);
875             $first_class = 0;
876             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
877             $seen_classes{$class} = 1;
878         }
879     }
880
881     for my $class ( keys %{$pkg->search_class_aliases} ) {
882
883         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
884             my $aliasr = qr/$alias/;
885             s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
886             s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
887             warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
888         }
889
890         if (!$compiled and !$seen_classes{$class}) {
891             $$r{search_class_re} .= '|' unless ($first_class);
892             $first_class = 0;
893
894             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
895             $seen_classes{$class} = 1;
896         }
897     }
898     $$r{search_class_re} .= '):' if (!$compiled);
899
900     warn '  'x$recursing." ** Rewritten query: $_\n" if $self->debug;
901
902     my $group_start = $pkg->operator('group_start');
903     my $group_end = $pkg->operator('group_end');
904     if (!$compiled) {
905         warn '  'x$recursing." ** Search class RE: $$r{search_class_re}\n" if $self->debug;
906
907         my $required_op = $pkg->operator('required');
908         $$r{required_re} = qr/\Q$required_op\E/;
909
910         my $disallowed_op = $pkg->operator('disallowed');
911         $$r{disallowed_re} = qr/\Q$disallowed_op\E/;
912
913         my $negated_op = $pkg->operator('negated');
914         $$r{negated_re} = qr/\Q$negated_op\E/;
915
916         my $and_op = $pkg->operator('and');
917         $$r{and_re} = qr/^\s*\Q$and_op\E/;
918
919         my $or_op = $pkg->operator('or');
920         $$r{or_re} = qr/^\s*\Q$or_op\E/;
921
922         $$r{group_start_re} = qr/^\s*($$r{negated_re}|$$r{disallowed_re})?\Q$group_start\E/;
923
924         $$r{group_end_re} = qr/^\s*\Q$group_end\E/;
925
926         my $float_start = $pkg->operator('float_start');
927         $$r{float_start_re} = qr/^\s*\Q$float_start\E/;
928
929         my $float_end = $pkg->operator('float_end');
930         $$r{float_end_re} = qr/^\s*\Q$float_end\E/;
931
932         $$r{atom_re} = qr/.+?(?=\Q$float_start\E|\Q$group_start\E|\Q$float_end\E|\Q$group_end\E|\s|"|$)/;
933
934         my $modifier_tag = $pkg->operator('modifier');
935         $$r{modifier_tag_re} = qr/^\s*\Q$modifier_tag\E/;
936
937         # Group start/end normally are ( and ), but can be overridden.
938         # We thus include ( and ) specifically due to filters, as well as : for classes.
939         $$r{phrase_cleanup_re} = qr/\s*(\Q$required_op\E|\Q$disallowed_op\E|\Q$and_op\E|\Q$or_op\E|\Q$group_start\E|\Q$group_end\E|\Q$float_start\E|\Q$float_end\E|\Q$modifier_tag\E|\Q$negated_op\E|:|\(|\))/;
940
941         # Build the filter and modifier uber-regexps
942         $$r{facet_re} = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\](?!\[)';
943
944         $$r{filter_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
945         $$r{filter_as_class_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
946
947         $$r{modifier_re} = '^\s*'.$$r{modifier_tag_re}.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
948         $$r{modifier_as_class_re} = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
949
950     }
951
952     my $struct = shift || $self->new_plan( level => $recursing );
953     $self->parse_tree( $struct ) if (!$self->parse_tree);
954
955     my $remainder = '';
956
957     my $loops = 0;
958     while (!$remainder) {
959         $loops++;
960         warn '  'x$recursing."Start of the loop. loop: $loops last_type: $last_type, joiner: ".$struct->joiner.", struct: $struct\n" if $self->debug;
961         if ($loops > 1000) { # the most magical of numbers...
962             warn '  'x$recursing." got to $loops loops; aborting\n" if $self->debug;
963             last;
964         }
965         if ($last_type eq 'FEND' and $fstart and $fstart !=  $struct) { # fall back further
966             $remainder = $_;
967             last;
968         } elsif ($last_type eq 'FEND') {
969             $fstart = undef;
970             $last_type = '';
971         }
972
973         if (/^\s*$/) { # end of an explicit group
974             $last_type = '';
975             last;
976         } elsif (/$$r{float_end_re}/) { # end of an explicit group
977             warn '  'x$recursing."Encountered explicit float end, remainder: $'\n" if $self->debug;
978
979             $remainder = $';
980             $_ = '';
981
982             $floating = 0;
983             $last_type = 'FEND';
984             last;
985         } elsif (/$$r{group_end_re}/) { # end of an explicit group
986             warn '  'x$recursing."Encountered explicit group end, remainder: $'\n" if $self->debug;
987
988             $remainder = $';
989             $_ = '';
990
991             $last_type = '';
992         } elsif ($self->filter_count && /$$r{filter_re}/) { # found a filter
993             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
994
995             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
996             $_ = $';
997
998             my $filter = $2;
999             my $params = [ split '[,]+', $3 ];
1000
1001             if ($pkg->filter_callbacks->{$filter}) {
1002                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1003                 $_ = "$replacement $_" if ($replacement);
1004             } else {
1005                 $struct->new_filter( $filter => $params, $negate );
1006             }
1007
1008
1009             $last_type = '';
1010         } elsif ($self->filter_count && /$$r{filter_as_class_re}/) { # found a filter
1011             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
1012
1013             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1014             $_ = $';
1015
1016             my $filter = $2;
1017             my $params = [ split '[,]+', $3 ];
1018
1019             if ($pkg->filter_callbacks->{$filter}) {
1020                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1021                 $_ = "$replacement $_" if ($replacement);
1022             } else {
1023                 $struct->new_filter( $filter => $params, $negate );
1024             }
1025
1026             $last_type = '';
1027         } elsif ($self->modifier_count && /$$r{modifier_re}/) { # found a modifier
1028             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1029
1030             $_ = $';
1031             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1032                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1033             } else {
1034                 $struct->new_modifier($1);
1035             }
1036
1037             $last_type = '';
1038         } elsif ($self->modifier_count && /$$r{modifier_as_class_re}/) { # found a modifier
1039             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1040
1041             my $mod = $1;
1042
1043             $_ = $';
1044             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1045                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1046             } elsif ($2 =~ /^[ty1]/i) {
1047                 $struct->new_modifier($mod);
1048             }
1049
1050             $last_type = '';
1051         } elsif (/$$r{float_start_re}/) { # start of an explicit float
1052             warn '  'x$recursing."Encountered explicit float start\n" if $self->debug;
1053             $floating = 1;
1054             $fstart = $struct;
1055
1056             $last_class = $current_class;
1057             $current_class = undef;
1058
1059             $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
1060
1061             # pass the floating_plan struct to be modified by the float'ed chunk
1062             my ($floating_plan, $subremainder) = $self->new( debug => $self->debug )->decompose( $', undef, undef, undef,  $self->floating_plan);
1063             $_ = $subremainder;
1064             warn '  'x$recursing."Remainder after explicit float: $_\n" if $self->debug;
1065
1066             $current_class = $last_class;
1067
1068             $last_type = '';
1069         } elsif (/$$r{group_start_re}/) { # start of an explicit group
1070             warn '  'x$recursing."Encountered explicit group start\n" if $self->debug;
1071
1072             if ($last_type eq 'CLASS') {
1073                 warn '  'x$recursing."Previous class change generated an empty node. Removing...\n" if $self->debug;
1074                 $struct->remove_last_node;
1075             }
1076
1077             my $negate = $1;
1078             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
1079             $substruct->negate(1) if ($substruct && $negate);
1080             $substruct->explicit(1) if ($substruct);
1081             $struct->add_node( $substruct ) if ($substruct);
1082             $_ = $subremainder;
1083             warn '  'x$recursing."Query remainder after bool group: $_\n" if $self->debug;
1084
1085             $last_type = '';
1086
1087         } elsif (/$$r{and_re}/) { # ANDed expression
1088             $_ = $';
1089             warn '  'x$recursing."Encountered AND\n" if $self->debug;
1090             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1091             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1092             $last_type = 'AND';
1093
1094             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1095             my $LHS = $struct;
1096             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1097             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 1 );
1098             $_ = $subremainder;
1099
1100             warn '  'x$recursing."RHS built\n" if $self->debug;
1101             warn '  'x$recursing."Post-AND remainder: $subremainder\n" if $self->debug;
1102
1103             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1104
1105             if ($LHS->floating) {
1106                 $wrapper->{query} = $LHS->{query};
1107                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1108                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1109                 $LHS->{query} = [$outer_wrapper];
1110                 $struct = $LHS;
1111             } else {
1112                 $wrapper->add_node($_) for ($LHS, $RHS);
1113                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1114                 $struct = $self->new_plan( level => $recursing );
1115                 $struct->add_node($wrapper);
1116             }
1117
1118             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1119
1120             $last_type = '';
1121         } elsif (/$$r{or_re}/) { # ORed expression
1122             $_ = $';
1123             warn '  'x$recursing."Encountered OR\n" if $self->debug;
1124             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1125             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1126             $last_type = 'OR';
1127
1128             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1129             my $LHS = $struct;
1130             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1131             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 2 );
1132             $_ = $subremainder;
1133
1134             warn '  'x$recursing."RHS built\n" if $self->debug;
1135             warn '  'x$recursing."Post-OR remainder: $subremainder\n" if $self->debug;
1136
1137             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1138
1139             if ($LHS->floating) {
1140                 $wrapper->{query} = $LHS->{query};
1141                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1142                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1143                 $LHS->{query} = [$outer_wrapper];
1144                 $struct = $LHS;
1145             } else {
1146                 $wrapper->add_node($_) for ($LHS, $RHS);
1147                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1148                 $struct = $self->new_plan( level => $recursing );
1149                 $struct->add_node($wrapper);
1150             }
1151
1152             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1153
1154             $last_type = '';
1155         } elsif ($self->facet_class_count && /$$r{facet_re}/) { # changing current class
1156             warn '  'x$recursing."Encountered facet: $1$2 => $3\n" if $self->debug;
1157
1158             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1159             my $facet = $2;
1160             my $facet_value = [ split '\s*\]\[\s*', $3 ];
1161             $struct->new_facet( $facet => $facet_value, $negate );
1162             $_ = $';
1163
1164             $last_type = '';
1165         } elsif ($self->search_class_count && /$$r{search_class_re}/) { # changing current class
1166
1167             if ($last_type eq 'CLASS') {
1168                 $struct->remove_last_node( $current_class );
1169                 warn '  'x$recursing."Encountered class change with no searches!\n" if $self->debug;
1170             }
1171
1172             warn '  'x$recursing."Encountered class change: $1\n" if $self->debug;
1173
1174             $current_class = $struct->classed_node( $1 )->requested_class();
1175             $_ = $';
1176
1177             $last_type = 'CLASS';
1178         } elsif (/^\s*($$r{required_re}|$$r{disallowed_re}|$$r{negated_re})?"([^"]+)(?:"|$)/) { # phrase, always anded
1179             warn '  'x$recursing.'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
1180
1181             my $req_ness = $1 || '';
1182             $req_ness = $pkg->operator('disallowed') if ($req_ness eq $pkg->operator('negated'));
1183             my $phrase = $2;
1184
1185             if (!$phrase_helper) {
1186                 warn '  'x$recursing."Recursing into decompose with the phrase as a subquery\n" if $self->debug;
1187                 my $after = $';
1188                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
1189                 $struct->add_node( $substruct ) if ($substruct);
1190                 $_ = $after;
1191             } else {
1192                 warn '  'x$recursing."Directly parsing the phrase [ $phrase ] subquery\n" if $self->debug;
1193                 $struct->joiner( '&' );
1194
1195                 my $class_node = $struct->classed_node($current_class);
1196
1197                 if ($req_ness eq $pkg->operator('disallowed')) {
1198                     $class_node->negate(1);
1199                 }
1200                 $class_node->add_phrase( $phrase );
1201
1202                 # Save $' before we clean up $phrase
1203                 my $temp_val = $';
1204
1205                 # Cleanup the phrase to make it so that we don't parse things in it as anything other than atoms
1206                 $phrase =~ s/$$r{phrase_cleanup_re}/ /g;
1207
1208                 $_ = $phrase . $temp_val;
1209
1210             }
1211
1212             $last_type = '';
1213
1214         } elsif (/^\s*($$r{required_re}|$$r{disallowed_re})($$r{atom_re})/) { # convert require/disallow word to {un}phrase
1215             warn '  'x$recursing."Encountered required atom (mini phrase), transforming for phrase parse: $1\n" if $self->debug;
1216
1217             $_ = $1 . '"' . $2 . '"' . $';
1218
1219             $last_type = '';
1220         } elsif (/^\s*($$r{atom_re})/) { # atom
1221             warn '  'x$recursing."Encountered atom: $1\n" if $self->debug;
1222             warn '  'x$recursing."Remainder: $'\n" if $self->debug;
1223
1224             my $atom = $1;
1225             my $after = $';
1226
1227             $_ = $after;
1228             $last_type = '';
1229
1230             my $class_node = $struct->classed_node($current_class);
1231
1232             my $prefix = ($atom =~ s/^$$r{negated_re}//o) ? '!' : '';
1233             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
1234
1235             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
1236 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$$r{required_re}//o);
1237
1238                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
1239                 $struct->joiner( '&' );
1240             }
1241
1242             $last_type = '';
1243         } else {
1244             warn '  'x$recursing."Cannot parse: $_\n" if $self->debug;
1245             $_ = '';
1246         }
1247
1248         last unless ($_);
1249
1250     }
1251
1252     $struct = undef if 
1253         scalar(@{$struct->query_nodes}) == 0 &&
1254         scalar(@{$struct->filters}) == 0 &&
1255         !$struct->top_plan;
1256
1257     return $struct if !wantarray;
1258     return ($struct, $remainder);
1259 }
1260
1261 =head2 find_class_index
1262
1263     $index = $QParser->find_class_index($class, $query);
1264 =cut
1265
1266 sub find_class_index {
1267     my $class = shift;
1268     my $query = shift;
1269
1270     my ($class_part, @field_parts) = split '\|', $class;
1271     $class_part ||= $class;
1272
1273     for my $idx ( 0 .. scalar(@$query) - 1 ) {
1274         next unless ref($$query[$idx]);
1275         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
1276     }
1277
1278     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
1279     return -1;
1280 }
1281
1282 =head2 core_limit
1283
1284     $limit = $QParser->core_limit([$limit]);
1285
1286 Return and/or set the core_limit.
1287 =cut
1288
1289 sub core_limit {
1290     my $self = shift;
1291     my $l = shift;
1292     $self->{core_limit} = $l if ($l);
1293     return $self->{core_limit};
1294 }
1295
1296 =head2 superpage
1297
1298     $superpage = $QParser->superpage([$superpage]);
1299
1300 Return and/or set the superpage.
1301 =cut
1302
1303 sub superpage {
1304     my $self = shift;
1305     my $l = shift;
1306     $self->{superpage} = $l if ($l);
1307     return $self->{superpage};
1308 }
1309
1310 =head2 superpage_size
1311
1312     $size = $QParser->superpage_size([$size]);
1313
1314 Return and/or set the superpage size.
1315 =cut
1316
1317 sub superpage_size {
1318     my $self = shift;
1319     my $l = shift;
1320     $self->{superpage_size} = $l if ($l);
1321     return $self->{superpage_size};
1322 }
1323
1324
1325 #-------------------------------
1326 package QueryParser::_util;
1327
1328 # At this level, joiners are always & or |.  This is not
1329 # the external, configurable representation of joiners that
1330 # defaults to # && and ||.
1331 sub is_joiner {
1332     my $str = shift;
1333
1334     return (not ref $str and ($str eq '&' or $str eq '|'));
1335 }
1336
1337 sub default_joiner { '&' }
1338
1339 # 0 for different, 1 for the same.
1340 sub compare_abstract_atoms {
1341     my ($left, $right) = @_;
1342
1343     foreach (qw/prefix suffix content/) {
1344         no warnings;    # undef can stand in for '' here
1345         return 0 unless $left->{$_} eq $right->{$_};
1346     }
1347
1348     return 1;
1349 }
1350
1351 sub fake_abstract_atom_from_phrase {
1352     my $phrase = shift;
1353     my $neg = shift;
1354     my $qp_class = shift || 'QueryParser';
1355
1356     my $prefix = '"';
1357     if ($neg) {
1358         $prefix =
1359             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
1360             $prefix;
1361     }
1362
1363     return {
1364         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
1365         "content" => $phrase
1366     }
1367 }
1368
1369 sub find_arrays_in_abstract {
1370     my ($hash) = @_;
1371
1372     my @arrays;
1373     foreach my $key (keys %$hash) {
1374         if (ref $hash->{$key} eq "ARRAY") {
1375             push @arrays, $hash->{$key};
1376             foreach (@{$hash->{$key}}) {
1377                 push @arrays, find_arrays_in_abstract($_);
1378             }
1379         }
1380     }
1381
1382     return @arrays;
1383 }
1384
1385 #-------------------------------
1386 package QueryParser::Canonicalize;  # not OO
1387 use Data::Dumper;
1388
1389 sub _abstract_query2str_filter {
1390     my $f = shift;
1391     my $qp_class = shift || 'QueryParser';
1392     my $qpconfig = $QueryParser::parser_config{$qp_class};
1393
1394     return sprintf(
1395         '%s%s(%s)',
1396         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
1397         $f->{name},
1398         join(",", @{$f->{args}})
1399     );
1400 }
1401
1402 sub _abstract_query2str_modifier {
1403     my $f = shift;
1404     my $qp_class = shift || 'QueryParser';
1405     my $qpconfig = $QueryParser::parser_config{$qp_class};
1406
1407     return $qpconfig->{operators}{modifier} . $f;
1408 }
1409
1410 sub _kid_list {
1411     my $children = shift;
1412     my $op = (keys %$children)[0];
1413     return @{$$children{$op}};
1414 }
1415
1416
1417 # This should produce an equivalent query to the original, given an
1418 # abstract_query.
1419 sub abstract_query2str_impl {
1420     my $abstract_query  = shift;
1421     my $depth = shift || 0;
1422
1423     my $qp_class ||= shift || 'QueryParser';
1424     my $force_qp_node = shift || 0;
1425     my $qpconfig = $QueryParser::parser_config{$qp_class};
1426
1427     my $fs = $qpconfig->{operators}{float_start};
1428     my $fe = $qpconfig->{operators}{float_end};
1429     my $gs = $qpconfig->{operators}{group_start};
1430     my $ge = $qpconfig->{operators}{group_end};
1431     my $and = $qpconfig->{operators}{and};
1432     my $or = $qpconfig->{operators}{or};
1433     my $ng = $qpconfig->{operators}{negated};
1434
1435     my $isnode = 0;
1436     my $negate = '';
1437     my $size = 0;
1438     my $q = "";
1439
1440     if (exists $abstract_query->{type}) {
1441         if ($abstract_query->{type} eq 'query_plan') {
1442             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
1443                 exists $abstract_query->{filters};
1444
1445             $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
1446                 exists $abstract_query->{modifiers};
1447
1448             $size = _kid_list($abstract_query->{children});
1449             if ($abstract_query->{negate}) {
1450                 $isnode = 1;
1451                 $negate = $ng;
1452             }
1453             $isnode = 1 if ($size > 1 and ($force_qp_node or $depth));
1454             #warn "size: $size, depth: $depth, isnode: $isnode, AQ: ".Dumper($abstract_query);
1455         } elsif ($abstract_query->{type} eq 'node') {
1456             if ($abstract_query->{alias}) {
1457                 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
1458                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
1459             } else {
1460                 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1461                 $q .= "|$_" foreach @{$abstract_query->{fields}};
1462             }
1463             $q .= ":";
1464             $isnode = 1;
1465         } elsif ($abstract_query->{type} eq 'atom') {
1466             my $prefix = $abstract_query->{prefix} || '';
1467             $prefix = $qpconfig->{operators}{negated} if $prefix eq '!';
1468             $q .= ($q ? ' ' : '') . $prefix .
1469                 ($abstract_query->{content} || '') .
1470                 ($abstract_query->{suffix} || '');
1471         } elsif ($abstract_query->{type} eq 'facet') {
1472             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1473             $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1474                 join("][", @{$abstract_query->{values}}) . "]";
1475         }
1476     }
1477
1478     my $next_depth = int($size > 1);
1479
1480     if (exists $abstract_query->{children}) {
1481
1482         my $op = (keys(%{$abstract_query->{children}}))[0];
1483
1484         if ($abstract_query->{floating}) { # always the top node!
1485             my $sub_node = pop @{$abstract_query->{children}{$op}};
1486
1487             $abstract_query->{floating} = 0;
1488             $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class, 1) . $fe. " ";
1489
1490             $abstract_query = $sub_node;
1491         }
1492
1493         if ($abstract_query && exists $abstract_query->{children}) {
1494             $op = (keys(%{$abstract_query->{children}}))[0];
1495             $q .= ($q ? ' ' : '') . join(
1496                 ($op eq '&' ? ' ' : " $or "),
1497                 map {
1498                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1499                 } @{$abstract_query->{children}{$op}}
1500             );
1501         }
1502     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1503         my $op = (keys(%{$abstract_query}))[0];
1504         $q .= ($q ? ' ' : '') . join(
1505             ($op eq '&' ? ' ' : " $or "),
1506             map {
1507                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1508             } @{$abstract_query->{$op}}
1509         );
1510     }
1511
1512     $q = "$gs$q$ge" if ($isnode);
1513     $q = $negate . $q if ($q);;
1514
1515     return $q;
1516 }
1517
1518 #-------------------------------
1519 package QueryParser::query_plan;
1520 use Data::Dumper;
1521 $Data::Dumper::Indent = 0;
1522
1523 sub atoms_only {
1524     my $self = shift;
1525     return @{$self->filters} == 0 &&
1526             @{$self->modifiers} == 0 &&
1527             @{[map { @{$_->phrases} } grep { ref($_) && $_->isa('QueryParser::query_plan::node')} @{$self->query_nodes}]} == 0
1528     ;
1529 }
1530
1531 sub _identical {
1532     my( $left, $right ) = @_;
1533     return 0 if scalar @$left != scalar @$right;
1534     my %hash;
1535     @hash{ @$left, @$right } = ();
1536     return scalar keys %hash == scalar @$left;
1537 }
1538
1539 sub pullup {
1540     my $self = shift;
1541     my $current_joiner = shift;
1542
1543     # burrow down until we our kids have no subqueries
1544     my $downlink_joiner;
1545     for my $qnode (@{ $self->query_nodes }) {
1546         $downlink_joiner = $qnode if (!ref($qnode));
1547         if (ref($qnode) && $qnode->can('pullup')) {
1548             $qnode->pullup($downlink_joiner);
1549         }
1550     }
1551
1552     warn "Entering pullup depth ". $self->plan_level . "\n"
1553         if $self->QueryParser->debug;
1554
1555     my $old_qnodes = $self->query_nodes; # we will ignore all but ::node objects in this list
1556     warn @$old_qnodes . " plans at pullup depth ". $self->plan_level . "\n"
1557         if $self->QueryParser->debug;
1558
1559     # PASS 0: If I only have one child, collapse filters/modifiers into me 
1560     if (@$old_qnodes == 1) {
1561         my $kid = $$old_qnodes[0];
1562         if ($kid->isa('QueryParser::query_plan')) {
1563             $self->add_filter($_) foreach @{$kid->filters};
1564             $self->add_facet($_) foreach @{$kid->facets};
1565             $self->add_modifier($_) foreach @{$kid->modifiers};
1566             $kid->{filters} = [];
1567             $kid->{facets} = [];
1568             $kid->{modifiers} = [];
1569
1570             my $kid_qnodes = $kid->query_nodes;
1571             if (@$kid_qnodes == 1) { # And if my kid is a plan with only one node, absorb that
1572                 my $a = $$kid_qnodes[0];
1573                 if ($a->isa('QueryParser::query_plan')) {
1574                     $self->{query} = [$a];
1575                     return $self;
1576                 }
1577             }
1578         }
1579     }
1580
1581     # PASS 1: loop, attempting to pull up simple nodes
1582     my @new_nodes;
1583     my $prev_node;
1584     my $prev_op;
1585
1586     my $prev_joiner;
1587
1588     while (my $p = shift(@$old_qnodes)) {
1589
1590         # joiners and ::node's get pushed onto the stack of new nodes
1591         if (!ref($p) or !$p->isa('QueryParser::query_plan')) {
1592             push @new_nodes, $p;
1593             next;
1594         }
1595
1596         # keep explicit and floating plans
1597         if ($p->explicit or $p->floating) {
1598             push @new_nodes, $p;
1599             next;
1600         }
1601
1602         if ($p->atoms_only) {
1603
1604             # 1-node plans get pulled up regardless of the plan's joiner
1605             if (@{$p->query_nodes} == 1) {
1606                 for my $a (@{$p->query_nodes}) {
1607                     if (ref($a) and $a->can('plan')) {
1608                         $a->plan($self);
1609                     }
1610                     push @new_nodes, $a;
1611                 }
1612                 next;
1613             }
1614
1615             # gather the joiners
1616             my %joiners = ( '&' => 0, '|' => 0 );
1617             my @nodelist = @{$p->query_nodes};
1618             while (my $n = shift(@nodelist)) {
1619                 next if ref($n); # only look at joiners
1620                 $joiners{$n}++;
1621             }
1622
1623             if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # mix of joiners? stop
1624                 if ($joiners{$self->joiner} > 0) { # needs to be our joiner in use
1625                     for my $a (@{$p->query_nodes}) {
1626                         if (ref($a) and $a->can('plan')) {
1627                             $a->plan($self);
1628                         }
1629                         push @new_nodes, $a;
1630                     }
1631                     next;
1632                 }
1633             }
1634         }
1635
1636         # default is to keep the whole plan
1637         push @new_nodes, $p;
1638     }
1639                 
1640     warn @new_nodes . " nodes after pullup of simple nodes at depth ". $self->plan_level . "\n"
1641         if $self->QueryParser->debug;
1642
1643     # PASS 2: merge adjacent ::node's
1644     my $dangling = 0;
1645     my $sync_node = $prev_joiner = undef;
1646     $old_qnodes = [@new_nodes];
1647     @new_nodes = ();
1648     while ( my $n = shift(@$old_qnodes) ) {
1649
1650         # joiners
1651         if (!ref($n)) {
1652             $prev_joiner = $current_joiner;
1653             $current_joiner = $n;
1654             warn "Joiner, recording it. [$prev_joiner => $current_joiner]\n" if $self->QueryParser->debug;
1655             next;
1656         }
1657
1658         # ::plan's etc get pushed onto the stack of new nodes
1659         if (!$n->isa('QueryParser::query_plan::node')) {
1660             push @new_nodes, $current_joiner if (@new_nodes);
1661             push @new_nodes, $n;
1662             $sync_node = undef;
1663             warn "Not a ::node, pushing onto the stack [$n]\n" if $self->QueryParser->debug;
1664             next;
1665         }
1666
1667         # grab the current target node
1668         if (!$sync_node) {
1669             warn "No sync_node, picking a new one\n" if $self->QueryParser->debug;
1670             $sync_node = $n;
1671             push @new_nodes, $current_joiner if (@new_nodes);
1672             push @new_nodes, $n;
1673             next;
1674         }
1675
1676         if (@{$n->query_atoms} == 0) {
1677             warn "weird ... empty node ...skipping\n" if $self->QueryParser->debug;
1678             push @new_nodes, $current_joiner if (@new_nodes);
1679             shift @$old_qnodes;
1680             next;
1681         }
1682
1683         my $sync_joiner = $sync_node->effective_joiner;
1684         my $n_joiner = $n->effective_joiner;
1685
1686         # plans of a different class or field set stay where they are
1687         if ($sync_node->classname ne $n->classname or !_identical($sync_node->fields,$n->fields)) {
1688             warn "Class/Field change! Need a new sync_node\n" if $self->QueryParser->debug;
1689             push @new_nodes, $current_joiner;
1690             push @new_nodes, $n;
1691             $sync_node = $n;
1692             $dangling = 1;
1693             next;
1694         }
1695
1696         if (!$sync_joiner or !$n_joiner) { # a node has a mix ... can't merge either
1697             warn "Mixed joiners, need a new sync_node\n" if $self->QueryParser->debug;
1698             push @new_nodes, $current_joiner;
1699             push @new_nodes, $n;
1700             $sync_node = $n;
1701             $dangling = 1;
1702             next;
1703         } elsif ($sync_joiner ne $n_joiner) { # different joiners, can't merge
1704             warn "Differing joiners, need a new sync_node\n" if $self->QueryParser->debug;
1705             push @new_nodes, $current_joiner;
1706             push @new_nodes, $n;
1707             $sync_node = $n;
1708             $dangling = 1;
1709             next;
1710         }
1711
1712         # we can push the next ::node's atoms onto our stack
1713         push @{$sync_node->query_atoms}, $current_joiner;
1714         for my $a (@{$n->query_atoms}) {
1715             if (ref($a)) {
1716                 $a->{node} = $sync_node;
1717             }
1718             push @{$sync_node->query_atoms}, $a;
1719         }
1720
1721         warn "Merged ".@{$n->query_atoms}." atoms into sync_node\n" if $self->QueryParser->debug;
1722         $dangling = 0;
1723
1724     }
1725
1726     push @new_nodes, $sync_node if ($dangling && $sync_node != $new_nodes[-1]);
1727    
1728     warn @new_nodes . " nodes at pullup depth ". $self->plan_level . " after compression\n"
1729         if $self->QueryParser->debug;
1730
1731     $self->{query} = \@new_nodes;
1732     return $self;
1733 }
1734
1735 sub QueryParser {
1736     my $self = shift;
1737     return undef unless ref($self);
1738     return $self->{QueryParser};
1739 }
1740
1741 sub new {
1742     my $pkg = shift;
1743     $pkg = ref($pkg) || $pkg;
1744     my %args = (query => [], joiner => '&', @_);
1745
1746     return bless \%args => $pkg;
1747 }
1748
1749 sub new_node {
1750     my $self = shift;
1751     my $pkg = ref($self) || $self;
1752     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1753     $self->add_node( $node );
1754     return $node;
1755 }
1756
1757 sub new_facet {
1758     my $self = shift;
1759     my $pkg = ref($self) || $self;
1760     my $name = shift;
1761     my $args = shift;
1762     my $negate = shift;
1763
1764     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1765     $self->add_node( $node );
1766
1767     return $node;
1768 }
1769
1770 sub new_filter {
1771     my $self = shift;
1772     my $pkg = ref($self) || $self;
1773     my $name = shift;
1774     my $args = shift;
1775     my $negate = shift;
1776
1777     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1778     $self->add_filter( $node );
1779
1780     return $node;
1781 }
1782
1783
1784 sub _merge_filters {
1785     my $left_filter = shift;
1786     my $right_filter = shift;
1787     my $join = shift;
1788
1789     return undef unless $left_filter or $right_filter;
1790     return $right_filter unless $left_filter;
1791     return $left_filter unless $right_filter;
1792
1793     my $args = $left_filter->{args} || [];
1794
1795     if ($join eq '|') {
1796         push(@$args, @{$right_filter->{args}});
1797
1798     } else {
1799         # find the intersect values
1800         my %new_vals;
1801         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1802         $args = [ grep { $new_vals{$_} } @$args ];
1803     }
1804
1805     $left_filter->{args} = $args;
1806     return $left_filter;
1807 }
1808
1809 sub collapse_filters {
1810     my $self = shift;
1811     my $name = shift;
1812
1813     # start by merging any filters at this level.
1814     # like-level filters are always ORed together
1815
1816     my $cur_filter;
1817     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1818     if (@cur_filters) {
1819         $cur_filter = shift @cur_filters;
1820         my $args = $cur_filter->{args} || [];
1821         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1822     }
1823
1824     # next gather the collapsed filters from sub-plans and 
1825     # merge them with our own
1826
1827     my @subquery = @{$self->{query}};
1828
1829     while (@subquery) {
1830         my $blob = shift @subquery;
1831         shift @subquery; # joiner
1832         next unless $blob->isa('QueryParser::query_plan');
1833         my $sub_filter = $blob->collapse_filters($name);
1834         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1835     }
1836
1837     if ($self->QueryParser->debug) {
1838         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1839         warn "collapse_filters($name) => [@args]\n";
1840     }
1841
1842     return $cur_filter;
1843 }
1844
1845 sub find_filter {
1846     my $self = shift;
1847     my $needle = shift;;
1848     return undef unless ($needle);
1849
1850     my $filter = $self->collapse_filters($needle);
1851
1852     warn "find_filter($needle) => " . 
1853         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1854         if $self->QueryParser->debug;
1855
1856     return $filter ? ($filter) : ();
1857 }
1858
1859 sub find_modifier {
1860     my $self = shift;
1861     my $needle = shift;;
1862     return undef unless ($needle);
1863     return grep { $_->name eq $needle } @{ $self->modifiers };
1864 }
1865
1866 sub new_modifier {
1867     my $self = shift;
1868     my $pkg = ref($self) || $self;
1869     my $name = shift;
1870
1871     my $node = do{$pkg.'::modifier'}->new( $name );
1872     $self->add_modifier( $node );
1873
1874     return $node;
1875 }
1876
1877 sub classed_node {
1878     my $self = shift;
1879     my $requested_class = shift;
1880
1881     my $node;
1882     for my $n (@{$self->{query}}) {
1883         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1884         if ($n->requested_class eq $requested_class) {
1885             $node = $n;
1886             last;
1887         }
1888     }
1889
1890     if (!$node) {
1891         $node = $self->new_node;
1892         $node->requested_class( $requested_class );
1893     }
1894
1895     return $node;
1896 }
1897
1898 sub remove_last_node {
1899     my $self = shift;
1900     my $requested_class = shift;
1901
1902     my $old = pop(@{$self->query_nodes});
1903     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1904
1905     return $old;
1906 }
1907
1908 sub query_nodes {
1909     my $self = shift;
1910     return $self->{query};
1911 }
1912
1913 sub floating {
1914     my $self = shift;
1915     my $f = shift;
1916     $self->{floating} = $f if (defined $f);
1917     return $self->{floating};
1918 }
1919
1920 sub explicit {
1921     my $self = shift;
1922     my $f = shift;
1923     $self->{explicit} = $f if (defined $f);
1924     return $self->{explicit};
1925 }
1926
1927 sub add_node {
1928     my $self = shift;
1929     my $node = shift;
1930
1931     $self->{query} ||= [];
1932     if ($node) {
1933         push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1934         push(@{$self->{query}}, $node);
1935     }
1936
1937     return $self;
1938 }
1939
1940 sub top_plan {
1941     my $self = shift;
1942
1943     return $self->{level} ? 0 : 1;
1944 }
1945
1946 sub plan_level {
1947     my $self = shift;
1948     my $level = shift;
1949
1950     if (defined $level) {
1951         $self->{level} = $level;
1952         for (@{$self->query_nodes}) {
1953             $_->plan_level($level + 1) if (ref and $_->isa('QueryParser::query_plan'));
1954         }
1955     }
1956             
1957     return $self->{level};
1958 }
1959
1960 sub joiner {
1961     my $self = shift;
1962     my $joiner = shift;
1963
1964     $self->{joiner} = $joiner if ($joiner);
1965     return $self->{joiner};
1966 }
1967
1968 sub modifiers {
1969     my $self = shift;
1970     $self->{modifiers} ||= [];
1971     return $self->{modifiers};
1972 }
1973
1974 sub add_modifier {
1975     my $self = shift;
1976     my $modifier = shift;
1977
1978     $self->{modifiers} ||= [];
1979     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1980
1981     push(@{$self->{modifiers}}, $modifier);
1982
1983     return $self;
1984 }
1985
1986 sub facets {
1987     my $self = shift;
1988     $self->{facets} ||= [];
1989     return $self->{facets};
1990 }
1991
1992 sub add_facet {
1993     my $self = shift;
1994     my $facet = shift;
1995
1996     $self->{facets} ||= [];
1997     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1998
1999     push(@{$self->{facets}}, $facet);
2000
2001     return $self;
2002 }
2003
2004 sub filters {
2005     my $self = shift;
2006     $self->{filters} ||= [];
2007     return $self->{filters};
2008 }
2009
2010 sub add_filter {
2011     my $self = shift;
2012     my $filter = shift;
2013
2014     $self->{filters} ||= [];
2015
2016     push(@{$self->{filters}}, $filter);
2017
2018     return $self;
2019 }
2020
2021 sub negate {
2022     my $self = shift;
2023     my $negate = shift;
2024
2025     $self->{negate} = $negate if (defined $negate);
2026
2027     return $self->{negate};
2028 }
2029
2030 # %opts supports two options at this time:
2031 #   no_phrases :
2032 #       If true, do not do anything to the phrases
2033 #       fields on any discovered nodes.
2034 #   with_config :
2035 #       If true, also return the query parser config as part of the blob.
2036 #       This will get set back to 0 before recursion to avoid repetition.
2037 sub to_abstract_query {
2038     my $self = shift;
2039     my %opts = @_;
2040
2041     my $pkg = ref $self->QueryParser || $self->QueryParser;
2042
2043     my $abstract_query = {
2044         type => "query_plan",
2045         floating => $self->floating,
2046         level => $self->plan_level,
2047         filters => [map { $_->to_abstract_query } @{$self->filters}],
2048         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}],
2049         negate => $self->negate
2050     };
2051
2052     if ($opts{with_config}) {
2053         $opts{with_config} = 0;
2054         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
2055     }
2056
2057     my $kids = [];
2058
2059     for my $qnode (@{$self->query_nodes}) {
2060         # Remember: qnode can be a joiner string, a node, or another query_plan
2061
2062         if (QueryParser::_util::is_joiner($qnode)) {
2063             if ($abstract_query->{children}) {
2064                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2065                 next if $open_joiner eq $qnode;
2066
2067                 my $oldroot = $abstract_query->{children};
2068                 $kids = [$oldroot];
2069                 $abstract_query->{children} = {$qnode => $kids};
2070             } else {
2071                 $abstract_query->{children} = {$qnode => $kids};
2072             }
2073         } else {
2074             push @$kids, $qnode->to_abstract_query(%opts);
2075         }
2076     }
2077
2078     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2079     return $abstract_query;
2080 }
2081
2082
2083 #-------------------------------
2084 package QueryParser::query_plan::node;
2085 use Data::Dumper;
2086 $Data::Dumper::Indent = 0;
2087
2088 sub effective_joiner {
2089     my $node = shift;
2090
2091     my @nodelist = @{$node->query_atoms};
2092     return $node->plan->joiner if (@nodelist == 1);
2093
2094     # gather the joiners
2095     my %joiners = ( '&' => 0, '|' => 0 );
2096     while (my $n = shift(@nodelist)) {
2097         next if ref($n); # only look at joiners
2098         $joiners{$n}++;
2099     }
2100
2101     if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # no mix of joiners
2102         return '|' if ($joiners{'|'});
2103         return '&';
2104     }
2105
2106     return undef;
2107 }
2108
2109 sub new {
2110     my $pkg = shift;
2111     $pkg = ref($pkg) || $pkg;
2112     my %args = @_;
2113
2114     return bless \%args => $pkg;
2115 }
2116
2117 sub new_atom {
2118     my $self = shift;
2119     my $pkg = ref($self) || $self;
2120     return do{$pkg.'::atom'}->new( @_ );
2121 }
2122
2123 sub requested_class { # also split into classname, fields and alias
2124     my $self = shift;
2125     my $class = shift;
2126
2127     if ($class) {
2128         my @afields;
2129         my (undef, $alias) = split '#', $class;
2130         if ($alias) {
2131             $class =~ s/#[^|]+//;
2132             ($alias, @afields) = split '\|', $alias;
2133         }
2134
2135         my @fields = @afields;
2136         my ($class_part, @field_parts) = split '\|', $class;
2137         for my $f (@field_parts) {
2138              push(@fields, $f) unless (grep { $f eq $_ } @fields);
2139         }
2140
2141         $class_part ||= $class;
2142
2143         $self->{requested_class} = $class;
2144         $self->{alias} = $alias if $alias;
2145         $self->{alias_fields} = \@afields if $alias;
2146         $self->{classname} = $class_part;
2147         $self->{fields} = \@fields;
2148     }
2149
2150     return $self->{requested_class};
2151 }
2152
2153 sub plan {
2154     my $self = shift;
2155     my $plan = shift;
2156
2157     $self->{plan} = $plan if ($plan);
2158     return $self->{plan};
2159 }
2160
2161 sub alias {
2162     my $self = shift;
2163     my $alias = shift;
2164
2165     $self->{alias} = $alias if ($alias);
2166     return $self->{alias};
2167 }
2168
2169 sub alias_fields {
2170     my $self = shift;
2171     my $alias = shift;
2172
2173     $self->{alias_fields} = $alias if ($alias);
2174     return $self->{alias_fields};
2175 }
2176
2177 sub classname {
2178     my $self = shift;
2179     my $class = shift;
2180
2181     $self->{classname} = $class if ($class);
2182     return $self->{classname};
2183 }
2184
2185 sub fields {
2186     my $self = shift;
2187     my @fields = @_;
2188
2189     $self->{fields} ||= [];
2190     $self->{fields} = \@fields if (@fields);
2191     return $self->{fields};
2192 }
2193
2194 sub phrases {
2195     my $self = shift;
2196     my @phrases = @_;
2197
2198     $self->{phrases} ||= [];
2199     $self->{phrases} = \@phrases if (@phrases);
2200     return $self->{phrases};
2201 }
2202
2203 sub add_phrase {
2204     my $self = shift;
2205     my $phrase = shift;
2206
2207     push(@{$self->phrases}, $phrase);
2208
2209     return $self;
2210 }
2211
2212 sub negate {
2213     my $self = shift;
2214     my $negate = shift;
2215
2216     $self->{negate} = $negate if (defined $negate);
2217
2218     return $self->{negate};
2219 }
2220
2221 sub query_atoms {
2222     my $self = shift;
2223     my @query_atoms = @_;
2224
2225     $self->{query_atoms} ||= [];
2226     $self->{query_atoms} = \@query_atoms if (@query_atoms);
2227     return $self->{query_atoms};
2228 }
2229
2230 sub add_fts_atom {
2231     my $self = shift;
2232     my $atom = shift;
2233
2234     if (!ref($atom)) {
2235         my $content = $atom;
2236         my @parts = @_;
2237
2238         $atom = $self->new_atom( content => $content, @parts );
2239     }
2240
2241     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2242     push(@{$self->query_atoms}, $atom);
2243
2244     return $self;
2245 }
2246
2247 sub add_dummy_atom {
2248     my $self = shift;
2249     my @parts = @_;
2250
2251     my $atom = $self->new_atom( @parts, dummy => 1 );
2252
2253     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2254     push(@{$self->query_atoms}, $atom);
2255
2256     return $self;
2257 }
2258
2259 # This will find up to one occurence of @$short_list within @$long_list, and
2260 # replace it with the single atom $replacement.
2261 sub replace_phrase_in_abstract_query {
2262     my ($self, $short_list, $long_list, $replacement) = @_;
2263
2264     my $success = 0;
2265     my @already = ();
2266     my $goal = scalar @$short_list;
2267
2268     for (my $i = 0; $i < scalar (@$long_list); $i++) {
2269         my $right = $long_list->[$i];
2270
2271         if (QueryParser::_util::compare_abstract_atoms(
2272             $short_list->[scalar @already], $right
2273         )) {
2274             push @already, $i;
2275         } elsif (scalar @already) {
2276             @already = ();
2277             next;
2278         }
2279
2280         if (scalar @already == $goal) {
2281             splice @$long_list, $already[0], scalar(@already), $replacement;
2282             $success = 1;
2283             last;
2284         }
2285     }
2286
2287     return $success;
2288 }
2289
2290 sub to_abstract_query {
2291     my $self = shift;
2292     my %opts = @_;
2293
2294     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
2295
2296     my $abstract_query = {
2297         "type" => "node",
2298         "alias" => $self->alias,
2299         "alias_fields" => $self->alias_fields,
2300         "class" => $self->classname,
2301         "fields" => $self->fields
2302     };
2303
2304     my $kids = [];
2305
2306     for my $qatom (@{$self->query_atoms}) {
2307         if (QueryParser::_util::is_joiner($qatom)) {
2308             if ($abstract_query->{children}) {
2309                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2310                 next if $open_joiner eq $qatom;
2311
2312                 my $oldroot = $abstract_query->{children};
2313                 $kids = [$oldroot];
2314                 $abstract_query->{children} = {$qatom => $kids};
2315             } else {
2316                 $abstract_query->{children} = {$qatom => $kids};
2317             }
2318         } else {
2319             push @$kids, $qatom->to_abstract_query;
2320         }
2321     }
2322
2323     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2324
2325     if ($self->{phrases} and not $opts{no_phrases}) {
2326         for my $phrase (@{$self->{phrases}}) {
2327             # Phrases appear duplication in a real QP tree, and we don't want
2328             # that duplication in our abstract query.  So for all our phrases,
2329             # break them into atoms as QP would, and remove any matching
2330             # sequences of atoms from our abstract query.
2331
2332             my $tmp_prefix = '';
2333             $tmp_prefix = $QueryParser::parser_config{$pkg}{operators}{disallowed} if ($self->{negate});
2334
2335             my $tmptree = $self->{plan}->{QueryParser}->new(query => $tmp_prefix.'"'.$phrase.'"')->parse->parse_tree;
2336             if ($tmptree) {
2337                 # For a well-behaved phrase, we should now have only one node
2338                 # in the $tmptree query plan, and that node should have an
2339                 # orderly list of atoms and joiners.
2340
2341                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2342                     my $tmplist;
2343
2344                     eval {
2345                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2346                             no_phrases => 1
2347                         )->{children}->{'&'}->[0]->{children}->{'&'};
2348                     };
2349                     next if $@;
2350
2351                     foreach (
2352                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
2353                     ) {
2354                         last if $self->replace_phrase_in_abstract_query(
2355                             $tmplist,
2356                             $_,
2357                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, $self->{negate}, $pkg)
2358                         );
2359                     }
2360                 }
2361             }
2362         }
2363     }
2364
2365     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2366     return $abstract_query;
2367 }
2368
2369 #-------------------------------
2370 package QueryParser::query_plan::node::atom;
2371
2372 sub new {
2373     my $pkg = shift;
2374     $pkg = ref($pkg) || $pkg;
2375     my %args = @_;
2376
2377     return bless \%args => $pkg;
2378 }
2379
2380 sub node {
2381     my $self = shift;
2382     return undef unless (ref $self);
2383     return $self->{node};
2384 }
2385
2386 sub content {
2387     my $self = shift;
2388     return undef unless (ref $self);
2389     return $self->{content};
2390 }
2391
2392 sub prefix {
2393     my $self = shift;
2394     return undef unless (ref $self);
2395     return $self->{prefix};
2396 }
2397
2398 sub suffix {
2399     my $self = shift;
2400     return undef unless (ref $self);
2401     return $self->{suffix};
2402 }
2403
2404 sub to_abstract_query {
2405     my ($self) = @_;
2406     
2407     return {
2408         (map { $_ => $self->$_ } qw/prefix suffix content/),
2409         "type" => "atom"
2410     };
2411 }
2412 #-------------------------------
2413 package QueryParser::query_plan::filter;
2414
2415 sub new {
2416     my $pkg = shift;
2417     $pkg = ref($pkg) || $pkg;
2418     my %args = @_;
2419
2420     return bless \%args => $pkg;
2421 }
2422
2423 sub plan {
2424     my $self = shift;
2425     return $self->{plan};
2426 }
2427
2428 sub name {
2429     my $self = shift;
2430     return $self->{name};
2431 }
2432
2433 sub negate {
2434     my $self = shift;
2435     return $self->{negate};
2436 }
2437
2438 sub args {
2439     my $self = shift;
2440     return $self->{args};
2441 }
2442
2443 sub to_abstract_query {
2444     my ($self) = @_;
2445     
2446     return {
2447         map { $_ => $self->$_ } qw/name negate args/
2448     };
2449 }
2450
2451 #-------------------------------
2452 package QueryParser::query_plan::facet;
2453
2454 sub new {
2455     my $pkg = shift;
2456     $pkg = ref($pkg) || $pkg;
2457     my %args = @_;
2458
2459     return bless \%args => $pkg;
2460 }
2461
2462 sub plan {
2463     my $self = shift;
2464     return $self->{plan};
2465 }
2466
2467 sub name {
2468     my $self = shift;
2469     return $self->{name};
2470 }
2471
2472 sub negate {
2473     my $self = shift;
2474     return $self->{negate};
2475 }
2476
2477 sub values {
2478     my $self = shift;
2479     return $self->{'values'};
2480 }
2481
2482 sub to_abstract_query {
2483     my ($self) = @_;
2484
2485     return {
2486         (map { $_ => $self->$_ } qw/name negate values/),
2487         "type" => "facet"
2488     };
2489 }
2490
2491 #-------------------------------
2492 package QueryParser::query_plan::modifier;
2493
2494 sub new {
2495     my $pkg = shift;
2496     $pkg = ref($pkg) || $pkg;
2497     my $modifier = shift;
2498     my $negate = shift;
2499
2500     return bless { name => $modifier, negate => $negate } => $pkg;
2501 }
2502
2503 sub name {
2504     my $self = shift;
2505     return $self->{name};
2506 }
2507
2508 sub negate {
2509     my $self = shift;
2510     return $self->{negate};
2511 }
2512
2513 sub to_abstract_query {
2514     my ($self) = @_;
2515     
2516     return $self->name;
2517 }
2518 1;
2519