]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
LP2045292 Color contrast for AngularJS patron bills
[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 get_abstract_data {
1524     my $self = shift;
1525     my $key = shift;
1526     return $self->{abstract_data}{$key};
1527 }
1528
1529 sub set_abstract_data {
1530     my $self = shift;
1531     my $key = shift;
1532     my $value = shift;
1533     $self->{abstract_data}{$key} = $value;
1534 }
1535
1536 sub atoms_only {
1537     my $self = shift;
1538     return @{$self->filters} == 0 &&
1539             @{$self->modifiers} == 0 &&
1540             @{[map { @{$_->phrases} } grep { ref($_) && $_->isa('QueryParser::query_plan::node')} @{$self->query_nodes}]} == 0
1541     ;
1542 }
1543
1544 sub _identical {
1545     my( $left, $right ) = @_;
1546     return 0 if scalar @$left != scalar @$right;
1547     my %hash;
1548     @hash{ @$left, @$right } = ();
1549     return scalar keys %hash == scalar @$left;
1550 }
1551
1552 sub pullup {
1553     my $self = shift;
1554     my $current_joiner = shift;
1555
1556     # burrow down until we our kids have no subqueries
1557     my $downlink_joiner;
1558     for my $qnode (@{ $self->query_nodes }) {
1559         $downlink_joiner = $qnode if (!ref($qnode));
1560         if (ref($qnode) && $qnode->can('pullup')) {
1561             $qnode->pullup($downlink_joiner);
1562         }
1563     }
1564
1565     warn "Entering pullup depth ". $self->plan_level . "\n"
1566         if $self->QueryParser->debug;
1567
1568     my $old_qnodes = $self->query_nodes; # we will ignore all but ::node objects in this list
1569     warn @$old_qnodes . " plans at pullup depth ". $self->plan_level . "\n"
1570         if $self->QueryParser->debug;
1571
1572     # PASS 0: If I only have one child, collapse filters/modifiers into me 
1573     if (@$old_qnodes == 1) {
1574         my $kid = $$old_qnodes[0];
1575         if ($kid->isa('QueryParser::query_plan')) {
1576             $self->add_filter($_) foreach @{$kid->filters};
1577             $self->add_facet($_) foreach @{$kid->facets};
1578             $self->add_modifier($_) foreach @{$kid->modifiers};
1579             $kid->{filters} = [];
1580             $kid->{facets} = [];
1581             $kid->{modifiers} = [];
1582
1583             my $kid_qnodes = $kid->query_nodes;
1584             if (@$kid_qnodes == 1) { # And if my kid is a plan with only one node, absorb that
1585                 my $a = $$kid_qnodes[0];
1586                 if ($a->isa('QueryParser::query_plan')) {
1587                     $self->{query} = [$a];
1588                     return $self;
1589                 }
1590             }
1591         }
1592     }
1593
1594     # PASS 1: loop, attempting to pull up simple nodes
1595     my @new_nodes;
1596     my $prev_node;
1597     my $prev_op;
1598
1599     my $prev_joiner;
1600
1601     while (my $p = shift(@$old_qnodes)) {
1602
1603         # joiners and ::node's get pushed onto the stack of new nodes
1604         if (!ref($p) or !$p->isa('QueryParser::query_plan')) {
1605             push @new_nodes, $p;
1606             next;
1607         }
1608
1609         # keep explicit and floating plans
1610         if ($p->explicit or $p->floating) {
1611             push @new_nodes, $p;
1612             next;
1613         }
1614
1615         if ($p->atoms_only) {
1616
1617             # 1-node plans get pulled up regardless of the plan's joiner
1618             if (@{$p->query_nodes} == 1) {
1619                 for my $a (@{$p->query_nodes}) {
1620                     if (ref($a) and $a->can('plan')) {
1621                         $a->plan($self);
1622                     }
1623                     push @new_nodes, $a;
1624                 }
1625                 next;
1626             }
1627
1628             # gather the joiners
1629             my %joiners = ( '&' => 0, '|' => 0 );
1630             my @nodelist = @{$p->query_nodes};
1631             while (my $n = shift(@nodelist)) {
1632                 next if ref($n); # only look at joiners
1633                 $joiners{$n}++;
1634             }
1635
1636             if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # mix of joiners? stop
1637                 if ($joiners{$self->joiner} > 0) { # needs to be our joiner in use
1638                     for my $a (@{$p->query_nodes}) {
1639                         if (ref($a) and $a->can('plan')) {
1640                             $a->plan($self);
1641                         }
1642                         push @new_nodes, $a;
1643                     }
1644                     next;
1645                 }
1646             }
1647         }
1648
1649         # default is to keep the whole plan
1650         push @new_nodes, $p;
1651     }
1652                 
1653     warn @new_nodes . " nodes after pullup of simple nodes at depth ". $self->plan_level . "\n"
1654         if $self->QueryParser->debug;
1655
1656     # PASS 2: merge adjacent ::node's
1657     my $dangling = 0;
1658     my $sync_node = $prev_joiner = undef;
1659     $old_qnodes = [@new_nodes];
1660     @new_nodes = ();
1661     while ( my $n = shift(@$old_qnodes) ) {
1662
1663         # joiners
1664         if (!ref($n)) {
1665             $prev_joiner = $current_joiner;
1666             $current_joiner = $n;
1667             warn "Joiner, recording it. [$prev_joiner => $current_joiner]\n" if $self->QueryParser->debug;
1668             next;
1669         }
1670
1671         # ::plan's etc get pushed onto the stack of new nodes
1672         if (!$n->isa('QueryParser::query_plan::node')) {
1673             push @new_nodes, $current_joiner if (@new_nodes);
1674             push @new_nodes, $n;
1675             $sync_node = undef;
1676             warn "Not a ::node, pushing onto the stack [$n]\n" if $self->QueryParser->debug;
1677             next;
1678         }
1679
1680         # grab the current target node
1681         if (!$sync_node) {
1682             warn "No sync_node, picking a new one\n" if $self->QueryParser->debug;
1683             $sync_node = $n;
1684             push @new_nodes, $current_joiner if (@new_nodes);
1685             push @new_nodes, $n;
1686             next;
1687         }
1688
1689         if (@{$n->query_atoms} == 0) {
1690             warn "weird ... empty node ...skipping\n" if $self->QueryParser->debug;
1691             push @new_nodes, $current_joiner if (@new_nodes);
1692             shift @$old_qnodes;
1693             next;
1694         }
1695
1696         my $sync_joiner = $sync_node->effective_joiner;
1697         my $n_joiner = $n->effective_joiner;
1698
1699         # plans of a different class or field set stay where they are
1700         if ($sync_node->classname ne $n->classname or !_identical($sync_node->fields,$n->fields)) {
1701             warn "Class/Field change! Need a new sync_node\n" if $self->QueryParser->debug;
1702             push @new_nodes, $current_joiner;
1703             push @new_nodes, $n;
1704             $sync_node = $n;
1705             $dangling = 1;
1706             next;
1707         }
1708
1709         if (!$sync_joiner or !$n_joiner) { # a node has a mix ... can't merge either
1710             warn "Mixed joiners, need a new sync_node\n" if $self->QueryParser->debug;
1711             push @new_nodes, $current_joiner;
1712             push @new_nodes, $n;
1713             $sync_node = $n;
1714             $dangling = 1;
1715             next;
1716         } elsif ($sync_joiner ne $n_joiner) { # different joiners, can't merge
1717             warn "Differing joiners, need a new sync_node\n" if $self->QueryParser->debug;
1718             push @new_nodes, $current_joiner;
1719             push @new_nodes, $n;
1720             $sync_node = $n;
1721             $dangling = 1;
1722             next;
1723         }
1724
1725         # we can push the next ::node's atoms onto our stack
1726         push @{$sync_node->query_atoms}, $current_joiner;
1727         for my $a (@{$n->query_atoms}) {
1728             if (ref($a)) {
1729                 $a->{node} = $sync_node;
1730             }
1731             push @{$sync_node->query_atoms}, $a;
1732         }
1733
1734         warn "Merged ".@{$n->query_atoms}." atoms into sync_node\n" if $self->QueryParser->debug;
1735         $dangling = 0;
1736
1737     }
1738
1739     push @new_nodes, $sync_node if ($dangling && $sync_node != $new_nodes[-1]);
1740    
1741     warn @new_nodes . " nodes at pullup depth ". $self->plan_level . " after compression\n"
1742         if $self->QueryParser->debug;
1743
1744     $self->{query} = \@new_nodes;
1745     return $self;
1746 }
1747
1748 sub QueryParser {
1749     my $self = shift;
1750     return undef unless ref($self);
1751     return $self->{QueryParser};
1752 }
1753
1754 sub new {
1755     my $pkg = shift;
1756     $pkg = ref($pkg) || $pkg;
1757     my %args = (abstract_data => {}, query => [], joiner => '&', @_);
1758
1759     return bless \%args => $pkg;
1760 }
1761
1762 sub new_node {
1763     my $self = shift;
1764     my $pkg = ref($self) || $self;
1765     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1766     $self->add_node( $node );
1767     return $node;
1768 }
1769
1770 sub new_facet {
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.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1778     $self->add_node( $node );
1779
1780     return $node;
1781 }
1782
1783 sub new_filter {
1784     my $self = shift;
1785     my $pkg = ref($self) || $self;
1786     my $name = shift;
1787     my $args = shift;
1788     my $negate = shift;
1789
1790     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1791     $self->add_filter( $node );
1792
1793     return $node;
1794 }
1795
1796
1797 sub _merge_filters {
1798     my $left_filter = shift;
1799     my $right_filter = shift;
1800     my $join = shift;
1801
1802     return undef unless $left_filter or $right_filter;
1803     return $right_filter unless $left_filter;
1804     return $left_filter unless $right_filter;
1805
1806     my $args = $left_filter->{args} || [];
1807
1808     if ($join eq '|') {
1809         push(@$args, @{$right_filter->{args}});
1810
1811     } else {
1812         # find the intersect values
1813         my %new_vals;
1814         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1815         $args = [ grep { $new_vals{$_} } @$args ];
1816     }
1817
1818     $left_filter->{args} = $args;
1819     return $left_filter;
1820 }
1821
1822 sub collapse_filters {
1823     my $self = shift;
1824     my $name = shift;
1825
1826     # start by merging any filters at this level.
1827     # like-level filters are always ORed together
1828
1829     my $cur_filter;
1830     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1831     if (@cur_filters) {
1832         $cur_filter = shift @cur_filters;
1833         my $args = $cur_filter->{args} || [];
1834         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1835     }
1836
1837     # next gather the collapsed filters from sub-plans and 
1838     # merge them with our own
1839
1840     my @subquery = @{$self->{query}};
1841
1842     while (@subquery) {
1843         my $blob = shift @subquery;
1844         shift @subquery; # joiner
1845         next unless $blob->isa('QueryParser::query_plan');
1846         my $sub_filter = $blob->collapse_filters($name);
1847         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1848     }
1849
1850     if ($self->QueryParser->debug) {
1851         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1852         warn "collapse_filters($name) => [@args]\n";
1853     }
1854
1855     return $cur_filter;
1856 }
1857
1858 sub find_filter {
1859     my $self = shift;
1860     my $needle = shift;;
1861     return undef unless ($needle);
1862
1863     my $filter = $self->collapse_filters($needle);
1864
1865     warn "find_filter($needle) => " . 
1866         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1867         if $self->QueryParser->debug;
1868
1869     return $filter ? ($filter) : ();
1870 }
1871
1872 sub find_modifier {
1873     my $self = shift;
1874     my $needle = shift;;
1875     return undef unless ($needle);
1876     return grep { $_->name eq $needle } @{ $self->modifiers };
1877 }
1878
1879 sub new_modifier {
1880     my $self = shift;
1881     my $pkg = ref($self) || $self;
1882     my $name = shift;
1883
1884     my $node = do{$pkg.'::modifier'}->new( $name );
1885     $self->add_modifier( $node );
1886
1887     return $node;
1888 }
1889
1890 sub classed_node {
1891     my $self = shift;
1892     my $requested_class = shift;
1893
1894     my $node;
1895     for my $n (@{$self->{query}}) {
1896         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1897         if ($n->requested_class eq $requested_class) {
1898             $node = $n;
1899             last;
1900         }
1901     }
1902
1903     if (!$node) {
1904         $node = $self->new_node;
1905         $node->requested_class( $requested_class );
1906     }
1907
1908     return $node;
1909 }
1910
1911 sub remove_last_node {
1912     my $self = shift;
1913     my $requested_class = shift;
1914
1915     my $old = pop(@{$self->query_nodes});
1916     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1917
1918     return $old;
1919 }
1920
1921 sub query_nodes {
1922     my $self = shift;
1923     return $self->{query};
1924 }
1925
1926 sub floating {
1927     my $self = shift;
1928     my $f = shift;
1929     $self->{floating} = $f if (defined $f);
1930     return $self->{floating};
1931 }
1932
1933 sub explicit {
1934     my $self = shift;
1935     my $f = shift;
1936     $self->{explicit} = $f if (defined $f);
1937     return $self->{explicit};
1938 }
1939
1940 sub add_node {
1941     my $self = shift;
1942     my $node = shift;
1943
1944     $self->{query} ||= [];
1945     if ($node) {
1946         push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1947         push(@{$self->{query}}, $node);
1948     }
1949
1950     return $self;
1951 }
1952
1953 sub top_plan {
1954     my $self = shift;
1955
1956     return $self->{level} ? 0 : 1;
1957 }
1958
1959 sub plan_level {
1960     my $self = shift;
1961     my $level = shift;
1962
1963     if (defined $level) {
1964         $self->{level} = $level;
1965         for (@{$self->query_nodes}) {
1966             $_->plan_level($level + 1) if (ref and $_->isa('QueryParser::query_plan'));
1967         }
1968     }
1969             
1970     return $self->{level};
1971 }
1972
1973 sub joiner {
1974     my $self = shift;
1975     my $joiner = shift;
1976
1977     $self->{joiner} = $joiner if ($joiner);
1978     return $self->{joiner};
1979 }
1980
1981 sub modifiers {
1982     my $self = shift;
1983     $self->{modifiers} ||= [];
1984     return $self->{modifiers};
1985 }
1986
1987 sub add_modifier {
1988     my $self = shift;
1989     my $modifier = shift;
1990
1991     $self->{modifiers} ||= [];
1992     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1993
1994     push(@{$self->{modifiers}}, $modifier);
1995
1996     return $self;
1997 }
1998
1999 sub facets {
2000     my $self = shift;
2001     $self->{facets} ||= [];
2002     return $self->{facets};
2003 }
2004
2005 sub add_facet {
2006     my $self = shift;
2007     my $facet = shift;
2008
2009     $self->{facets} ||= [];
2010     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
2011
2012     push(@{$self->{facets}}, $facet);
2013
2014     return $self;
2015 }
2016
2017 sub filters {
2018     my $self = shift;
2019     $self->{filters} ||= [];
2020     return $self->{filters};
2021 }
2022
2023 sub add_filter {
2024     my $self = shift;
2025     my $filter = shift;
2026
2027     $self->{filters} ||= [];
2028
2029     push(@{$self->{filters}}, $filter);
2030
2031     return $self;
2032 }
2033
2034 sub negate {
2035     my $self = shift;
2036     my $negate = shift;
2037
2038     $self->{negate} = $negate if (defined $negate);
2039
2040     return $self->{negate};
2041 }
2042
2043 # %opts supports two options at this time:
2044 #   no_phrases :
2045 #       If true, do not do anything to the phrases
2046 #       fields on any discovered nodes.
2047 #   with_config :
2048 #       If true, also return the query parser config as part of the blob.
2049 #       This will get set back to 0 before recursion to avoid repetition.
2050 sub to_abstract_query {
2051     my $self = shift;
2052     my %opts = @_;
2053
2054     my $pkg = ref $self->QueryParser || $self->QueryParser;
2055
2056     my $abstract_query = {
2057         type => "query_plan",
2058         floating => $self->floating,
2059         level => $self->plan_level,
2060         filters => [map { $_->to_abstract_query } @{$self->filters}],
2061         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}],
2062         negate => $self->negate
2063     };
2064
2065     if ($opts{with_config}) {
2066         $opts{with_config} = 0;
2067         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
2068     }
2069
2070     my $kids = [];
2071
2072     for my $qnode (@{$self->query_nodes}) {
2073         # Remember: qnode can be a joiner string, a node, or another query_plan
2074
2075         if (QueryParser::_util::is_joiner($qnode)) {
2076             if ($abstract_query->{children}) {
2077                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2078                 next if $open_joiner eq $qnode;
2079
2080                 my $oldroot = $abstract_query->{children};
2081                 $kids = [$oldroot];
2082                 $abstract_query->{children} = {$qnode => $kids};
2083             } else {
2084                 $abstract_query->{children} = {$qnode => $kids};
2085             }
2086         } else {
2087             push @$kids, $qnode->to_abstract_query(%opts);
2088         }
2089     }
2090
2091     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2092     $$abstract_query{additional_data} = $self->{abstract_data}
2093         if (keys(%{$self->{abstract_data}}));
2094
2095     return $abstract_query;
2096 }
2097
2098
2099 #-------------------------------
2100 package QueryParser::query_plan::node;
2101 use Data::Dumper;
2102 $Data::Dumper::Indent = 0;
2103
2104 sub effective_joiner {
2105     my $node = shift;
2106
2107     my @nodelist = @{$node->query_atoms};
2108     return $node->plan->joiner if (@nodelist == 1);
2109
2110     # gather the joiners
2111     my %joiners = ( '&' => 0, '|' => 0 );
2112     while (my $n = shift(@nodelist)) {
2113         next if ref($n); # only look at joiners
2114         $joiners{$n}++;
2115     }
2116
2117     if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # no mix of joiners
2118         return '|' if ($joiners{'|'});
2119         return '&';
2120     }
2121
2122     return undef;
2123 }
2124
2125 sub new {
2126     my $pkg = shift;
2127     $pkg = ref($pkg) || $pkg;
2128     my %args = @_;
2129
2130     return bless \%args => $pkg;
2131 }
2132
2133 sub new_atom {
2134     my $self = shift;
2135     my $pkg = ref($self) || $self;
2136     return do{$pkg.'::atom'}->new( @_ );
2137 }
2138
2139 sub requested_class { # also split into classname, fields and alias
2140     my $self = shift;
2141     my $class = shift;
2142
2143     if ($class) {
2144         my @afields;
2145         my (undef, $alias) = split '#', $class;
2146         if ($alias) {
2147             $class =~ s/#[^|]+//;
2148             ($alias, @afields) = split '\|', $alias;
2149         }
2150
2151         my @fields = @afields;
2152         my ($class_part, @field_parts) = split '\|', $class;
2153         for my $f (@field_parts) {
2154              push(@fields, $f) unless (grep { $f eq $_ } @fields);
2155         }
2156
2157         $class_part ||= $class;
2158
2159         $self->{requested_class} = $class;
2160         $self->{alias} = $alias if $alias;
2161         $self->{alias_fields} = \@afields if $alias;
2162         $self->{classname} = $class_part;
2163         $self->{fields} = \@fields;
2164     }
2165
2166     return $self->{requested_class};
2167 }
2168
2169 sub plan {
2170     my $self = shift;
2171     my $plan = shift;
2172
2173     $self->{plan} = $plan if ($plan);
2174     return $self->{plan};
2175 }
2176
2177 sub alias {
2178     my $self = shift;
2179     my $alias = shift;
2180
2181     $self->{alias} = $alias if ($alias);
2182     return $self->{alias};
2183 }
2184
2185 sub alias_fields {
2186     my $self = shift;
2187     my $alias = shift;
2188
2189     $self->{alias_fields} = $alias if ($alias);
2190     return $self->{alias_fields};
2191 }
2192
2193 sub classname {
2194     my $self = shift;
2195     my $class = shift;
2196
2197     $self->{classname} = $class if ($class);
2198     return $self->{classname};
2199 }
2200
2201 sub fields {
2202     my $self = shift;
2203     my @fields = @_;
2204
2205     $self->{fields} ||= [];
2206     $self->{fields} = \@fields if (@fields);
2207     return $self->{fields};
2208 }
2209
2210 sub phrases {
2211     my $self = shift;
2212     my @phrases = @_;
2213
2214     $self->{phrases} ||= [];
2215     $self->{phrases} = \@phrases if (@phrases);
2216     return $self->{phrases};
2217 }
2218
2219 sub add_phrase {
2220     my $self = shift;
2221     my $phrase = shift;
2222
2223     push(@{$self->phrases}, $phrase);
2224
2225     return $self;
2226 }
2227
2228 sub negate {
2229     my $self = shift;
2230     my $negate = shift;
2231
2232     $self->{negate} = $negate if (defined $negate);
2233
2234     return $self->{negate};
2235 }
2236
2237 sub query_atoms {
2238     my $self = shift;
2239     my @query_atoms = @_;
2240
2241     $self->{query_atoms} ||= [];
2242     $self->{query_atoms} = \@query_atoms if (@query_atoms);
2243     return $self->{query_atoms};
2244 }
2245
2246 sub add_fts_atom {
2247     my $self = shift;
2248     my $atom = shift;
2249
2250     if (!ref($atom)) {
2251         my $content = $atom;
2252         my @parts = @_;
2253
2254         $atom = $self->new_atom( content => $content, @parts );
2255     }
2256
2257     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2258     push(@{$self->query_atoms}, $atom);
2259
2260     return $self;
2261 }
2262
2263 sub add_dummy_atom {
2264     my $self = shift;
2265     my @parts = @_;
2266
2267     my $atom = $self->new_atom( @parts, dummy => 1 );
2268
2269     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2270     push(@{$self->query_atoms}, $atom);
2271
2272     return $self;
2273 }
2274
2275 # This will find up to one occurence of @$short_list within @$long_list, and
2276 # replace it with the single atom $replacement.
2277 sub replace_phrase_in_abstract_query {
2278     my ($self, $short_list, $long_list, $replacement) = @_;
2279
2280     my $success = 0;
2281     my @already = ();
2282     my $goal = scalar @$short_list;
2283
2284     for (my $i = 0; $i < scalar (@$long_list); $i++) {
2285         my $right = $long_list->[$i];
2286
2287         if (QueryParser::_util::compare_abstract_atoms(
2288             $short_list->[scalar @already], $right
2289         )) {
2290             push @already, $i;
2291         } elsif (scalar @already) {
2292             @already = ();
2293             next;
2294         }
2295
2296         if (scalar @already == $goal) {
2297             splice @$long_list, $already[0], scalar(@already), $replacement;
2298             $success = 1;
2299             last;
2300         }
2301     }
2302
2303     return $success;
2304 }
2305
2306 sub to_abstract_query {
2307     my $self = shift;
2308     my %opts = @_;
2309
2310     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
2311
2312     my $abstract_query = {
2313         "type" => "node",
2314         "alias" => $self->alias,
2315         "alias_fields" => $self->alias_fields,
2316         "class" => $self->classname,
2317         "fields" => $self->fields
2318     };
2319
2320     $self->abstract_node_additions($abstract_query)
2321         if ($self->can('abstract_node_additions'));
2322
2323     my $kids = [];
2324
2325     for my $qatom (@{$self->query_atoms}) {
2326         if (QueryParser::_util::is_joiner($qatom)) {
2327             if ($abstract_query->{children}) {
2328                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2329                 next if $open_joiner eq $qatom;
2330
2331                 my $oldroot = $abstract_query->{children};
2332                 $kids = [$oldroot];
2333                 $abstract_query->{children} = {$qatom => $kids};
2334             } else {
2335                 $abstract_query->{children} = {$qatom => $kids};
2336             }
2337         } else {
2338             push @$kids, $qatom->to_abstract_query;
2339         }
2340     }
2341
2342     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2343
2344     if ($self->{phrases} and not $opts{no_phrases}) {
2345         for my $phrase (@{$self->{phrases}}) {
2346             # Phrases appear duplication in a real QP tree, and we don't want
2347             # that duplication in our abstract query.  So for all our phrases,
2348             # break them into atoms as QP would, and remove any matching
2349             # sequences of atoms from our abstract query.
2350
2351             my $tmp_prefix = '';
2352             $tmp_prefix = $QueryParser::parser_config{$pkg}{operators}{disallowed} if ($self->{negate});
2353
2354             my $tmptree = $self->{plan}->{QueryParser}->new(query => $tmp_prefix.'"'.$phrase.'"')->parse->parse_tree;
2355             if ($tmptree) {
2356                 # For a well-behaved phrase, we should now have only one node
2357                 # in the $tmptree query plan, and that node should have an
2358                 # orderly list of atoms and joiners.
2359
2360                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2361                     my $tmplist;
2362
2363                     eval {
2364                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2365                             no_phrases => 1
2366                         )->{children}->{'&'}->[0]->{children}->{'&'};
2367                     };
2368                     next if $@;
2369
2370                     foreach (
2371                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
2372                     ) {
2373                         last if $self->replace_phrase_in_abstract_query(
2374                             $tmplist,
2375                             $_,
2376                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, $self->{negate}, $pkg)
2377                         );
2378                     }
2379                 }
2380             }
2381         }
2382     }
2383
2384     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2385     return $abstract_query;
2386 }
2387
2388 #-------------------------------
2389 package QueryParser::query_plan::node::atom;
2390
2391 sub new {
2392     my $pkg = shift;
2393     $pkg = ref($pkg) || $pkg;
2394     my %args = @_;
2395
2396     return bless \%args => $pkg;
2397 }
2398
2399 sub node {
2400     my $self = shift;
2401     return undef unless (ref $self);
2402     return $self->{node};
2403 }
2404
2405 sub content {
2406     my $self = shift;
2407     return undef unless (ref $self);
2408     return $self->{content};
2409 }
2410
2411 sub prefix {
2412     my $self = shift;
2413     return undef unless (ref $self);
2414     return $self->{prefix};
2415 }
2416
2417 sub suffix {
2418     my $self = shift;
2419     return undef unless (ref $self);
2420     return $self->{suffix};
2421 }
2422
2423 sub to_abstract_query {
2424     my ($self) = @_;
2425     
2426     return {
2427         (map { $_ => $self->$_ } qw/prefix suffix content/),
2428         "type" => "atom"
2429     };
2430 }
2431 #-------------------------------
2432 package QueryParser::query_plan::filter;
2433
2434 sub new {
2435     my $pkg = shift;
2436     $pkg = ref($pkg) || $pkg;
2437     my %args = @_;
2438
2439     return bless \%args => $pkg;
2440 }
2441
2442 sub plan {
2443     my $self = shift;
2444     return $self->{plan};
2445 }
2446
2447 sub name {
2448     my $self = shift;
2449     return $self->{name};
2450 }
2451
2452 sub negate {
2453     my $self = shift;
2454     return $self->{negate};
2455 }
2456
2457 sub args {
2458     my $self = shift;
2459     return $self->{args};
2460 }
2461
2462 sub to_abstract_query {
2463     my ($self) = @_;
2464     
2465     return {
2466         map { $_ => $self->$_ } qw/name negate args/
2467     };
2468 }
2469
2470 #-------------------------------
2471 package QueryParser::query_plan::facet;
2472
2473 sub new {
2474     my $pkg = shift;
2475     $pkg = ref($pkg) || $pkg;
2476     my %args = @_;
2477
2478     return bless \%args => $pkg;
2479 }
2480
2481 sub plan {
2482     my $self = shift;
2483     return $self->{plan};
2484 }
2485
2486 sub name {
2487     my $self = shift;
2488     return $self->{name};
2489 }
2490
2491 sub negate {
2492     my $self = shift;
2493     return $self->{negate};
2494 }
2495
2496 sub values {
2497     my $self = shift;
2498     return $self->{'values'};
2499 }
2500
2501 sub to_abstract_query {
2502     my ($self) = @_;
2503
2504     return {
2505         (map { $_ => $self->$_ } qw/name negate values/),
2506         "type" => "facet"
2507     };
2508 }
2509
2510 #-------------------------------
2511 package QueryParser::query_plan::modifier;
2512
2513 sub new {
2514     my $pkg = shift;
2515     $pkg = ref($pkg) || $pkg;
2516     my $modifier = shift;
2517     my $negate = shift;
2518
2519     return bless { name => $modifier, negate => $negate } => $pkg;
2520 }
2521
2522 sub name {
2523     my $self = shift;
2524     return $self->{name};
2525 }
2526
2527 sub negate {
2528     my $self = shift;
2529     return $self->{negate};
2530 }
2531
2532 sub to_abstract_query {
2533     my ($self) = @_;
2534     
2535     return $self->name;
2536 }
2537 1;
2538