]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
LP1615805 No inputs after submit in patron search (AngularJS)
[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     warn "Query tree after pullup:\n" . Dumper($self->parse_tree) if $self->debug;
817     $self->parse_tree->plan_level(0);
818
819     return $self;
820 }
821
822 =head2 decompose
823
824     ($struct, $remainder) = $QParser->decompose($querystring, [$current_class], [$recursing], [$phrase_helper]);
825
826 This routine does the heavy work of parsing the query string recursively.
827 Returns the top level query plan, or the query plan from a lower level plus
828 the portion of the query string that needs to be processed at a higher level.
829 =cut
830
831 our $_compiled_decomposer = {};
832 sub decompose {
833     my $self = shift;
834     my $pkg = ref($self) || $self;
835
836     my $r = $$_compiled_decomposer{$pkg};
837     my $compiled = defined($r);
838
839     $_ = shift;
840     my $current_class = shift || $self->default_search_class;
841
842     my $recursing = shift || 0;
843     my $phrase_helper = shift || 0;
844
845     warn '  'x$recursing." ** QP: decompose package is $pkg" if $self->debug;
846
847     if (!$compiled) {
848         $r = $$_compiled_decomposer{$pkg} = {};
849         warn '  'x$recursing." ** Compiling decomposer\n" if $self->debug;
850
851         # Build the search class+field uber-regexp
852         $$r{search_class_re} = '^\s*(';
853     } else {
854         warn '  'x$recursing." ** Decomposer already compiled\n" if $self->debug;
855     }
856
857     my $first_class = 1;
858
859     my %seen_classes;
860     for my $class ( keys %{$pkg->search_field_aliases} ) {
861         warn '  'x$recursing." *** ... Looking for search fields in $class\n" if $self->debug;
862
863         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
864             warn '  'x$recursing." *** ... Looking for aliases of $field\n" if $self->debug;
865
866             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
867                 my $aliasr = qr/$alias/;
868                 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
869                 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
870                 warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
871             }
872         }
873
874         if (!$compiled) {
875             $$r{search_class_re} .= '|' unless ($first_class);
876             $first_class = 0;
877             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
878             $seen_classes{$class} = 1;
879         }
880     }
881
882     for my $class ( keys %{$pkg->search_class_aliases} ) {
883
884         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
885             my $aliasr = qr/$alias/;
886             s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
887             s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
888             warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
889         }
890
891         if (!$compiled and !$seen_classes{$class}) {
892             $$r{search_class_re} .= '|' unless ($first_class);
893             $first_class = 0;
894
895             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
896             $seen_classes{$class} = 1;
897         }
898     }
899     $$r{search_class_re} .= '):' if (!$compiled);
900
901     warn '  'x$recursing." ** Rewritten query: $_\n" if $self->debug;
902
903     my $group_start = $pkg->operator('group_start');
904     my $group_end = $pkg->operator('group_end');
905     if (!$compiled) {
906         warn '  'x$recursing." ** Search class RE: $$r{search_class_re}\n" if $self->debug;
907
908         my $required_op = $pkg->operator('required');
909         $$r{required_re} = qr/\Q$required_op\E/;
910
911         my $disallowed_op = $pkg->operator('disallowed');
912         $$r{disallowed_re} = qr/\Q$disallowed_op\E/;
913
914         my $negated_op = $pkg->operator('negated');
915         $$r{negated_re} = qr/\Q$negated_op\E/;
916
917         my $and_op = $pkg->operator('and');
918         $$r{and_re} = qr/^\s*\Q$and_op\E/;
919
920         my $or_op = $pkg->operator('or');
921         $$r{or_re} = qr/^\s*\Q$or_op\E/;
922
923         $$r{group_start_re} = qr/^\s*($$r{negated_re}|$$r{disallowed_re})?\Q$group_start\E/;
924
925         $$r{group_end_re} = qr/^\s*\Q$group_end\E/;
926
927         my $float_start = $pkg->operator('float_start');
928         $$r{float_start_re} = qr/^\s*\Q$float_start\E/;
929
930         my $float_end = $pkg->operator('float_end');
931         $$r{float_end_re} = qr/^\s*\Q$float_end\E/;
932
933         $$r{atom_re} = qr/.+?(?=\Q$float_start\E|\Q$group_start\E|\Q$float_end\E|\Q$group_end\E|\s|"|$)/;
934
935         my $modifier_tag = $pkg->operator('modifier');
936         $$r{modifier_tag_re} = qr/^\s*\Q$modifier_tag\E/;
937
938         # Group start/end normally are ( and ), but can be overridden.
939         # We thus include ( and ) specifically due to filters, as well as : for classes.
940         $$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|:|\(|\))/;
941
942         # Build the filter and modifier uber-regexps
943         $$r{facet_re} = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\](?!\[)';
944
945         $$r{filter_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
946         $$r{filter_as_class_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
947
948         $$r{modifier_re} = '^\s*'.$$r{modifier_tag_re}.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
949         $$r{modifier_as_class_re} = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
950
951     }
952
953     my $struct = shift || $self->new_plan( level => $recursing );
954     $self->parse_tree( $struct ) if (!$self->parse_tree);
955
956     my $remainder = '';
957
958     my $loops = 0;
959     while (!$remainder) {
960         $loops++;
961         warn '  'x$recursing."Start of the loop. loop: $loops last_type: $last_type, joiner: ".$struct->joiner.", struct: $struct\n" if $self->debug;
962         if ($loops > 1000) { # the most magical of numbers...
963             warn '  'x$recursing." got to $loops loops; aborting\n" if $self->debug;
964             last;
965         }
966         if ($last_type eq 'FEND' and $fstart and $fstart !=  $struct) { # fall back further
967             $remainder = $_;
968             last;
969         } elsif ($last_type eq 'FEND') {
970             $fstart = undef;
971             $last_type = '';
972         }
973
974         if (/^\s*$/) { # end of an explicit group
975             $last_type = '';
976             last;
977         } elsif (/$$r{float_end_re}/) { # end of an explicit group
978             warn '  'x$recursing."Encountered explicit float end, remainder: $'\n" if $self->debug;
979
980             $remainder = $';
981             $_ = '';
982
983             $floating = 0;
984             $last_type = 'FEND';
985             last;
986         } elsif (/$$r{group_end_re}/) { # end of an explicit group
987             warn '  'x$recursing."Encountered explicit group end, remainder: $'\n" if $self->debug;
988
989             if ($recursing) {
990                 $remainder = $';
991                 $_ = '';
992             } else {
993                 $_ = $';
994             }
995
996             $last_type = '';
997         } elsif ($self->filter_count && /$$r{filter_re}/) { # found a filter
998             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
999
1000             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1001             $_ = $';
1002
1003             my $filter = $2;
1004             my $params = [ split '[,]+', $3 ];
1005
1006             if ($pkg->filter_callbacks->{$filter}) {
1007                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1008                 $_ = "$replacement $_" if ($replacement);
1009             } else {
1010                 $struct->new_filter( $filter => $params, $negate );
1011             }
1012
1013
1014             $last_type = '';
1015         } elsif ($self->filter_count && /$$r{filter_as_class_re}/) { # found a filter
1016             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
1017
1018             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1019             $_ = $';
1020
1021             my $filter = $2;
1022             my $params = [ split '[,]+', $3 ];
1023
1024             if ($pkg->filter_callbacks->{$filter}) {
1025                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1026                 $_ = "$replacement $_" if ($replacement);
1027             } else {
1028                 $struct->new_filter( $filter => $params, $negate );
1029             }
1030
1031             $last_type = '';
1032         } elsif ($self->modifier_count && /$$r{modifier_re}/) { # found a modifier
1033             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1034
1035             $_ = $';
1036             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1037                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1038             } else {
1039                 $struct->new_modifier($1);
1040             }
1041
1042             $last_type = '';
1043         } elsif ($self->modifier_count && /$$r{modifier_as_class_re}/) { # found a modifier
1044             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1045
1046             my $mod = $1;
1047
1048             $_ = $';
1049             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1050                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1051             } elsif ($2 =~ /^[ty1]/i) {
1052                 $struct->new_modifier($mod);
1053             }
1054
1055             $last_type = '';
1056         } elsif (/$$r{float_start_re}/) { # start of an explicit float
1057             warn '  'x$recursing."Encountered explicit float start\n" if $self->debug;
1058             $floating = 1;
1059             $fstart = $struct;
1060
1061             $last_class = $current_class;
1062             $current_class = undef;
1063
1064             $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
1065
1066             # pass the floating_plan struct to be modified by the float'ed chunk
1067             my ($floating_plan, $subremainder) = $self->new( debug => $self->debug )->decompose( $', undef, undef, undef,  $self->floating_plan);
1068             $_ = $subremainder;
1069             warn '  'x$recursing."Remainder after explicit float: $_\n" if $self->debug;
1070
1071             $current_class = $last_class;
1072
1073             $last_type = '';
1074         } elsif (/$$r{group_start_re}/) { # start of an explicit group
1075             warn '  'x$recursing."Encountered explicit group start\n" if $self->debug;
1076
1077             if ($last_type eq 'CLASS') {
1078                 warn '  'x$recursing."Previous class change generated an empty node. Removing...\n" if $self->debug;
1079                 $struct->remove_last_node;
1080             }
1081
1082             my $negate = $1;
1083             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
1084             if ($substruct) {
1085                 $substruct->negate(1) if ($negate);
1086                 $substruct->explicit(1);
1087                 $struct->add_node( $substruct );
1088             }
1089             $_ = $subremainder;
1090             warn '  'x$recursing."Query remainder after bool group: $_\n" if $self->debug;
1091
1092             $last_type = '';
1093
1094         } elsif (/$$r{and_re}/) { # ANDed expression
1095             $_ = $';
1096             warn '  'x$recursing."Encountered AND\n" if $self->debug;
1097             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1098             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1099             $last_type = 'AND';
1100
1101             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1102             my $LHS = $struct;
1103             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1104             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 1 );
1105             $_ = $subremainder;
1106
1107             warn '  'x$recursing."RHS built\n" if $self->debug;
1108             warn '  'x$recursing."Post-AND remainder: $subremainder\n" if $self->debug;
1109
1110             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1111
1112             if ($LHS->floating) {
1113                 $wrapper->{query} = $LHS->{query};
1114                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1115                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1116                 $LHS->{query} = [$outer_wrapper];
1117                 $struct = $LHS;
1118             } else {
1119                 $wrapper->add_node($_) for ($LHS, $RHS);
1120                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1121                 $struct = $self->new_plan( level => $recursing );
1122                 $struct->add_node($wrapper);
1123             }
1124
1125             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1126
1127             $last_type = '';
1128         } elsif (/$$r{or_re}/) { # ORed expression
1129             $_ = $';
1130             warn '  'x$recursing."Encountered OR\n" if $self->debug;
1131             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1132             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1133             $last_type = 'OR';
1134
1135             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1136             my $LHS = $struct;
1137             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1138             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 2 );
1139             $remainder = $subremainder;
1140
1141             warn '  'x$recursing."RHS built\n" if $self->debug;
1142             warn '  'x$recursing."Post-OR remainder: $subremainder\n" if $self->debug;
1143
1144             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1145
1146             if ($LHS->floating) {
1147                 $wrapper->{query} = $LHS->{query};
1148                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1149                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1150                 $LHS->{query} = [$outer_wrapper];
1151                 $struct = $LHS;
1152             } else {
1153                 $wrapper->add_node($_) for ($LHS, $RHS);
1154                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1155                 $struct = $self->new_plan( level => $recursing );
1156                 $struct->add_node($wrapper);
1157             }
1158
1159             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1160
1161             $last_type = '';
1162         } elsif ($self->facet_class_count && /$$r{facet_re}/) { # changing current class
1163             warn '  'x$recursing."Encountered facet: $1$2 => $3\n" if $self->debug;
1164
1165             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1166             my $facet = $2;
1167             my $facet_value = [ split '\s*\]\[\s*', $3 ];
1168             $struct->new_facet( $facet => $facet_value, $negate );
1169             $_ = $';
1170
1171             $last_type = '';
1172         } elsif ($self->search_class_count && /$$r{search_class_re}/) { # changing current class
1173
1174             if ($last_type eq 'CLASS') {
1175                 $struct->remove_last_node( $current_class );
1176                 warn '  'x$recursing."Encountered class change with no searches!\n" if $self->debug;
1177             }
1178
1179             warn '  'x$recursing."Encountered class change: $1\n" if $self->debug;
1180
1181             $current_class = $struct->classed_node( $1 )->requested_class();
1182             $_ = $';
1183
1184             $last_type = 'CLASS';
1185         } elsif (/^\s*($$r{required_re}|$$r{disallowed_re}|$$r{negated_re})?"([^"]+)(?:"|$)/) { # phrase, always anded
1186             warn '  'x$recursing.'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
1187
1188             my $req_ness = $1 || '';
1189             $req_ness = $pkg->operator('disallowed') if ($req_ness eq $pkg->operator('negated'));
1190             my $phrase = $2;
1191
1192             if (!$phrase_helper) {
1193                 warn '  'x$recursing."Recursing into decompose with the phrase as a subquery\n" if $self->debug;
1194                 my $after = $';
1195                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
1196                 $struct->add_node( $substruct ) if ($substruct);
1197                 $_ = $after;
1198             } else {
1199                 warn '  'x$recursing."Directly parsing the phrase [ $phrase ] subquery\n" if $self->debug;
1200                 $struct->joiner( '&' );
1201
1202                 my $class_node = $struct->classed_node($current_class);
1203
1204                 if (grep { $req_ness eq $_ } ($pkg->operator('disallowed'), $pkg->operator('negated'))) {
1205                     $class_node->negate(1);
1206                     $req_ness = $pkg->operator('negated');
1207                 } else {
1208                     $req_ness = '';
1209                 }
1210                 $phrase =~ s/$$r{phrase_cleanup_re}/ /g;
1211                 $class_node->add_phrase( $phrase );
1212                 $class_node->add_dummy_atom;
1213
1214                 last;
1215             }
1216
1217             $last_type = '';
1218
1219         } elsif (/^\s*((?:$$r{required_re}|$$r{disallowed_re}|$$r{negated_re})?)($$r{atom_re})/) { # atoms
1220             warn '  'x$recursing."Encountered atom: $1\n" if $self->debug;
1221             warn '  'x$recursing."Remainder: $'\n" if $self->debug;
1222
1223             my $req_ness = $1;
1224             my $atom = $2;
1225             my $after = $';
1226
1227             $_ = $after;
1228             $last_type = '';
1229
1230             my $class_node = $struct->classed_node($current_class);
1231
1232             my $prefix = '';
1233             if ($req_ness) {
1234                 $prefix = ($req_ness =~ /^$$r{required_re}/) ? '' : '!';
1235             }
1236
1237             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
1238
1239             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
1240 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$$r{required_re}//o);
1241
1242                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
1243                 $struct->joiner( '&' );
1244             }
1245
1246             $last_type = '';
1247         } else {
1248             warn '  'x$recursing."Cannot parse: $_\n" if $self->debug;
1249             $_ = '';
1250         }
1251
1252         last unless ($_);
1253
1254     }
1255
1256     $struct = undef if 
1257         scalar(@{$struct->query_nodes}) == 0 &&
1258         scalar(@{$struct->filters}) == 0 &&
1259         !$struct->top_plan;
1260
1261     return $struct if !wantarray;
1262     return ($struct, $remainder);
1263 }
1264
1265 =head2 find_class_index
1266
1267     $index = $QParser->find_class_index($class, $query);
1268 =cut
1269
1270 sub find_class_index {
1271     my $class = shift;
1272     my $query = shift;
1273
1274     my ($class_part, @field_parts) = split '\|', $class;
1275     $class_part ||= $class;
1276
1277     for my $idx ( 0 .. scalar(@$query) - 1 ) {
1278         next unless ref($$query[$idx]);
1279         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
1280     }
1281
1282     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
1283     return -1;
1284 }
1285
1286 =head2 core_limit
1287
1288     $limit = $QParser->core_limit([$limit]);
1289
1290 Return and/or set the core_limit.
1291 =cut
1292
1293 sub core_limit {
1294     my $self = shift;
1295     my $l = shift;
1296     $self->{core_limit} = $l if ($l);
1297     return $self->{core_limit};
1298 }
1299
1300 =head2 superpage
1301
1302     $superpage = $QParser->superpage([$superpage]);
1303
1304 Return and/or set the superpage.
1305 =cut
1306
1307 sub superpage {
1308     my $self = shift;
1309     my $l = shift;
1310     $self->{superpage} = $l if ($l);
1311     return $self->{superpage};
1312 }
1313
1314 =head2 superpage_size
1315
1316     $size = $QParser->superpage_size([$size]);
1317
1318 Return and/or set the superpage size.
1319 =cut
1320
1321 sub superpage_size {
1322     my $self = shift;
1323     my $l = shift;
1324     $self->{superpage_size} = $l if ($l);
1325     return $self->{superpage_size};
1326 }
1327
1328
1329 #-------------------------------
1330 package QueryParser::_util;
1331
1332 # At this level, joiners are always & or |.  This is not
1333 # the external, configurable representation of joiners that
1334 # defaults to # && and ||.
1335 sub is_joiner {
1336     my $str = shift;
1337
1338     return (not ref $str and ($str eq '&' or $str eq '|'));
1339 }
1340
1341 sub default_joiner { '&' }
1342
1343 # 0 for different, 1 for the same.
1344 sub compare_abstract_atoms {
1345     my ($left, $right) = @_;
1346
1347     foreach (qw/prefix suffix content/) {
1348         no warnings;    # undef can stand in for '' here
1349         return 0 unless $left->{$_} eq $right->{$_};
1350     }
1351
1352     return 1;
1353 }
1354
1355 sub fake_abstract_atom_from_phrase {
1356     my $phrase = shift;
1357     my $neg = shift;
1358     my $qp_class = shift || 'QueryParser';
1359
1360     my $prefix = '"';
1361     if ($neg) {
1362         $prefix =
1363             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
1364             $prefix;
1365     }
1366
1367     return {
1368         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
1369         "content" => $phrase
1370     }
1371 }
1372
1373 sub find_arrays_in_abstract {
1374     my ($hash) = @_;
1375
1376     my @arrays;
1377     foreach my $key (keys %$hash) {
1378         if (ref $hash->{$key} eq "ARRAY") {
1379             push @arrays, $hash->{$key};
1380             foreach (@{$hash->{$key}}) {
1381                 push @arrays, find_arrays_in_abstract($_);
1382             }
1383         }
1384     }
1385
1386     return @arrays;
1387 }
1388
1389 #-------------------------------
1390 package QueryParser::Canonicalize;  # not OO
1391 use Data::Dumper;
1392
1393 sub _abstract_query2str_filter {
1394     my $f = shift;
1395     my $qp_class = shift || 'QueryParser';
1396     my $qpconfig = $QueryParser::parser_config{$qp_class};
1397
1398     return sprintf(
1399         '%s%s(%s)',
1400         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
1401         $f->{name},
1402         join(",", @{$f->{args}})
1403     );
1404 }
1405
1406 sub _abstract_query2str_modifier {
1407     my $f = shift;
1408     my $qp_class = shift || 'QueryParser';
1409     my $qpconfig = $QueryParser::parser_config{$qp_class};
1410
1411     return $qpconfig->{operators}{modifier} . $f;
1412 }
1413
1414 sub _kid_list {
1415     my $children = shift;
1416     my $op = (keys %$children)[0];
1417     return @{$$children{$op}};
1418 }
1419
1420
1421 # This should produce an equivalent query to the original, given an
1422 # abstract_query.
1423 sub abstract_query2str_impl {
1424     my $abstract_query  = shift;
1425     my $depth = shift || 0;
1426
1427     my $qp_class ||= shift || 'QueryParser';
1428     my $force_qp_node = shift || 0;
1429     my $qpconfig = $QueryParser::parser_config{$qp_class};
1430
1431     my $fs = $qpconfig->{operators}{float_start};
1432     my $fe = $qpconfig->{operators}{float_end};
1433     my $gs = $qpconfig->{operators}{group_start};
1434     my $ge = $qpconfig->{operators}{group_end};
1435     my $and = $qpconfig->{operators}{and};
1436     my $or = $qpconfig->{operators}{or};
1437     my $ng = $qpconfig->{operators}{negated};
1438
1439     my $isnode = 0;
1440     my $negate = '';
1441     my $size = 0;
1442     my $q = "";
1443
1444     if (exists $abstract_query->{type}) {
1445         if ($abstract_query->{type} eq 'query_plan') {
1446             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
1447                 exists $abstract_query->{filters};
1448
1449             $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
1450                 exists $abstract_query->{modifiers};
1451
1452             $size = _kid_list($abstract_query->{children});
1453             if ($abstract_query->{negate}) {
1454                 $isnode = 1;
1455                 $negate = $ng;
1456             }
1457             $isnode = 1 if ($size > 1 and ($force_qp_node or $depth));
1458             #warn "size: $size, depth: $depth, isnode: $isnode, AQ: ".Dumper($abstract_query);
1459         } elsif ($abstract_query->{type} eq 'node') {
1460             if ($abstract_query->{alias}) {
1461                 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
1462                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
1463             } else {
1464                 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1465                 $q .= "|$_" foreach @{$abstract_query->{fields}};
1466             }
1467             $q .= ":";
1468             $isnode = 1;
1469         } elsif ($abstract_query->{type} eq 'atom') {
1470             my $add_space = $q ? 1 : 0;
1471             if ($abstract_query->{explicit_start}) {
1472                 $q .= ' ' if $add_space;
1473                 $q .= $gs x $abstract_query->{explicit_start};
1474                 $add_space = 0;
1475             }
1476             my $prefix = $abstract_query->{prefix} || '';
1477             $prefix = $qpconfig->{operators}{negated} if $prefix eq '!';
1478             $q .= ($add_space ? ' ' : '') . $prefix .
1479                 ($abstract_query->{content} // '') .
1480                 ($abstract_query->{suffix} || '');
1481             $q .= $ge x $abstract_query->{explicit_end} if ($abstract_query->{explicit_end});
1482         } elsif ($abstract_query->{type} eq 'facet') {
1483             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1484             $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1485                 join("][", @{$abstract_query->{values}}) . "]";
1486         }
1487     }
1488
1489     my $next_depth = int($size > 1);
1490
1491     if (exists $abstract_query->{children}) {
1492
1493         my $op = (keys(%{$abstract_query->{children}}))[0];
1494
1495         if ($abstract_query->{floating}) { # always the top node!
1496             my $sub_node = pop @{$abstract_query->{children}{$op}};
1497
1498             $abstract_query->{floating} = 0;
1499             $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class, 1) . $fe. " ";
1500
1501             $abstract_query = $sub_node;
1502         }
1503
1504         if ($abstract_query && exists $abstract_query->{children}) {
1505             $op = (keys(%{$abstract_query->{children}}))[0];
1506             $q .= ($q ? ' ' : '') . join(
1507                 ($op eq '&' ? ' ' : " $or "),
1508                 map {
1509                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1510                 } @{$abstract_query->{children}{$op}}
1511             );
1512         }
1513     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1514         my $op = (keys(%{$abstract_query}))[0];
1515         $q .= ($q ? ' ' : '') . join(
1516             ($op eq '&' ? ' ' : " $or "),
1517             map {
1518                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1519             } @{$abstract_query->{$op}}
1520         );
1521     }
1522
1523     $q = "$gs$q$ge" if ($isnode);
1524     $q = $negate . $q if ($q);
1525
1526     return $q;
1527 }
1528
1529 #-------------------------------
1530 package QueryParser::query_plan;
1531 use Data::Dumper;
1532 $Data::Dumper::Indent = 0;
1533
1534 sub get_abstract_data {
1535     my $self = shift;
1536     my $key = shift;
1537     return $self->{abstract_data}{$key};
1538 }
1539
1540 sub set_abstract_data {
1541     my $self = shift;
1542     my $key = shift;
1543     my $value = shift;
1544     $self->{abstract_data}{$key} = $value;
1545 }
1546
1547 sub atoms_only {
1548     my $self = shift;
1549     return @{$self->filters} == 0 &&
1550             @{$self->modifiers} == 0 &&
1551             @{[map { @{$_->phrases} } grep { ref($_) && $_->isa('QueryParser::query_plan::node')} @{$self->query_nodes}]} == 0
1552     ;
1553 }
1554
1555 sub _identical {
1556     my( $left, $right ) = @_;
1557     return 0 if scalar @$left != scalar @$right;
1558     my %hash;
1559     @hash{ @$left, @$right } = ();
1560     return scalar keys %hash == scalar @$left;
1561 }
1562
1563 sub pullup {
1564     my $self = shift;
1565
1566     # burrow down until we our kids have no subqueries
1567     my $downlink_joiner;
1568     for my $qnode (@{ $self->query_nodes }) {
1569         $qnode->pullup() if (ref($qnode) && $qnode->can('pullup'));
1570     }
1571     warn "Entering pullup depth ". $self->plan_level . "\n" if $self->QueryParser->debug;
1572
1573     my $old_qnodes = $self->query_nodes;
1574     warn @$old_qnodes . " query nodes (plans, nodes) at pullup depth ". $self->plan_level . "\n"
1575         if $self->QueryParser->debug;
1576
1577     # Step 1: pull up subplan filter/facet/modifier nodes. These
1578     # will bubble up to the top of the plan tree.  Evergreen doesn't
1579     # support nested filter/facet/modifier constructs currently.
1580     for my $kid (@$old_qnodes) {
1581         if (ref($kid) and $kid->isa('QueryParser::query_plan')) {
1582             $self->add_filter($_) foreach @{$kid->filters};
1583             $self->add_facet($_) foreach @{$kid->facets};
1584             $self->add_modifier($_) foreach @{$kid->modifiers};
1585             $kid->{filters} = [];
1586             $kid->{facets} = [];
1587             $kid->{modifiers} = [];
1588         }
1589     }
1590
1591     # Step 2: Pull up ::nodes from subplans that only have nodes (no
1592     # nested subplans).  This is in preparation for adjacent node merge,
1593     # and because this is a depth-first recursion, we've already decided
1594     # if nested plans can be elided.
1595     my @new_nodes;
1596     while (my $kid = shift @$old_qnodes) {
1597         if (ref($kid) and $kid->isa('QueryParser::query_plan')) {
1598             my $kid_query_nodes = $kid->query_nodes;
1599             my @kid_notnodes = grep { ref($_) and !$_->isa('QueryParser::query_plan::node') } @$kid_query_nodes;
1600             my @kid_nodes = grep { ref($_) and $_->isa('QueryParser::query_plan::node') } @$kid_query_nodes;
1601             if (@kid_nodes and !@kid_notnodes) {
1602                 warn "pulling up nodes from nested plan at pullup depth ". $self->plan_level . "\n" if $self->QueryParser->debug;
1603                 push @new_nodes, map { $_->plan($self) if ref; $_ } @$kid_query_nodes;
1604                 next;
1605             }
1606         }
1607         push @new_nodes, $kid;
1608     }
1609
1610     # Step 3: Merge our adjacent ::nodes if they have the same requested_class.
1611     # This could miss merging aliased classes that are equiv, but that check
1612     # is more fiddly, and usually searches just use the class name.
1613     $old_qnodes = [@new_nodes];
1614     @new_nodes = ();
1615     while ( my $current_node = shift(@$old_qnodes) ) {
1616
1617         unless (@$old_qnodes) { # last node, no compression possible
1618             push @new_nodes, $current_node;
1619             last;
1620         }
1621
1622         my $current_joiner = shift(@$old_qnodes);
1623         my $next_node = shift(@$old_qnodes);
1624
1625         # if they're both nodes, see if we can merge them
1626         if ($current_node->isa('QueryParser::query_plan::node')
1627             and $next_node->isa('QueryParser::query_plan::node')
1628             and $current_node->requested_class eq $next_node->requested_class
1629             and (defined $current_node->negate ? $current_node->negate : "") 
1630                 eq (defined $next_node->negate ? $next_node->negate : "")
1631         ) {
1632             warn "merging RHS atoms into atom list for LHS with joiner $current_joiner\n" if $self->QueryParser->debug;
1633             push @{$current_node->query_atoms}, $current_joiner if @{$current_node->query_atoms};
1634             push @{$current_node->query_atoms}, map { if (ref($_)) { $_->{node} = $current_node }; $_ } @{$next_node->query_atoms};
1635             push @{$current_node->phrases}, @{$next_node->phrases};
1636             unshift @$old_qnodes, $current_node;
1637         } else {
1638             push @new_nodes, $current_node, $current_joiner;
1639             unshift @$old_qnodes, $next_node;
1640         }
1641     }
1642
1643     $self->{query} = \@new_nodes;
1644
1645     # Step 4: As soon as we can, apply the explicit markers directly
1646     # to ::atoms so that we retain that for canonicalization while
1647     # also clearing away useless explicit groupings.
1648     if ($self->explicit) {
1649         if (!grep { # we have no non-::node, non-joiner query nodes, we've become a same-class singlton
1650                 ref($_) and !$_->isa('QueryParser::query_plan::node')
1651             } @{$self->query_nodes}
1652             and 1 == grep { # and we have exactly one (possibly merged, above) ::node with at least one ::atom
1653                 ref($_) and $_->isa('QueryParser::query_plan::node')
1654             } @{$self->query_nodes}
1655             and (my @atoms = @{$self->query_nodes->[0]->query_atoms}) > 0
1656         ) {
1657             
1658                 warn "setting explicit flags on atoms that may later be pulled up, at depth". $self->plan_level . "\n"
1659                     if $self->QueryParser->debug;
1660                 my $first_atom = $atoms[0];
1661                 my $last_atom = $atoms[-1];
1662                 $first_atom->explicit_start(defined $first_atom->explicit_start ? $first_atom->explicit_start + 1 : 1);
1663                 $last_atom->explicit_end(defined $last_atom->explicit_end ? $last_atom->explicit_end + 1 : 1);
1664         } else { # otherwise, the explicit grouping is meaningless, toss it
1665             $self->explicit(0);
1666         }
1667     }
1668
1669     warn @new_nodes . " nodes at pullup depth ". $self->plan_level . " after compression\n" if $self->QueryParser->debug;
1670
1671     return $self;
1672 }
1673
1674 sub joiners {
1675     my $self = shift;
1676     my %list = map { ($_=>1) } grep {!ref($_)} @{$self->{query}};
1677     return keys %list;
1678 }
1679
1680 sub classes {
1681     my $self = shift;
1682     my %list = map {
1683         ($_->classname . '|' . join('|',sort($_->fields)) => 1)
1684     } grep {
1685         ref($_) and ref($_) =~ /::node$/
1686     } @{$self->{query}};
1687     return keys %list;
1688 }
1689
1690 sub QueryParser {
1691     my $self = shift;
1692     return undef unless ref($self);
1693     return $self->{QueryParser};
1694 }
1695
1696 sub new {
1697     my $pkg = shift;
1698     $pkg = ref($pkg) || $pkg;
1699     my %args = (abstract_data => {}, query => [], joiner => '&', @_);
1700
1701     return bless \%args => $pkg;
1702 }
1703
1704 sub new_node {
1705     my $self = shift;
1706     my $pkg = ref($self) || $self;
1707     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1708     $self->add_node( $node );
1709     return $node;
1710 }
1711
1712 sub new_facet {
1713     my $self = shift;
1714     my $pkg = ref($self) || $self;
1715     my $name = shift;
1716     my $args = shift;
1717     my $negate = shift;
1718
1719     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1720     $self->add_node( $node );
1721
1722     return $node;
1723 }
1724
1725 sub new_filter {
1726     my $self = shift;
1727     my $pkg = ref($self) || $self;
1728     my $name = shift;
1729     my $args = shift;
1730     my $negate = shift;
1731
1732     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1733     $self->add_filter( $node );
1734
1735     return $node;
1736 }
1737
1738
1739 sub _merge_filters {
1740     my $left_filter = shift;
1741     my $right_filter = shift;
1742     my $join = shift;
1743
1744     return undef unless $left_filter or $right_filter;
1745     return $right_filter unless $left_filter;
1746     return $left_filter unless $right_filter;
1747
1748     my $args = $left_filter->{args} || [];
1749
1750     if ($join eq '|') {
1751         push(@$args, @{$right_filter->{args}});
1752
1753     } else {
1754         # find the intersect values
1755         my %new_vals;
1756         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1757         $args = [ grep { $new_vals{$_} } @$args ];
1758     }
1759
1760     $left_filter->{args} = $args;
1761     return $left_filter;
1762 }
1763
1764 sub collapse_filters {
1765     my $self = shift;
1766     my $name = shift;
1767
1768     # start by merging any filters at this level.
1769     # like-level filters are always ORed together
1770
1771     my $cur_filter;
1772     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1773     if (@cur_filters) {
1774         $cur_filter = shift @cur_filters;
1775         my $args = $cur_filter->{args} || [];
1776         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1777     }
1778
1779     # next gather the collapsed filters from sub-plans and 
1780     # merge them with our own
1781
1782     my @subquery = @{$self->{query}};
1783
1784     while (@subquery) {
1785         my $blob = shift @subquery;
1786         shift @subquery; # joiner
1787         next unless $blob->isa('QueryParser::query_plan');
1788         my $sub_filter = $blob->collapse_filters($name);
1789         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1790     }
1791
1792     if ($self->QueryParser->debug) {
1793         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1794         warn "collapse_filters($name) => [@args]\n";
1795     }
1796
1797     return $cur_filter;
1798 }
1799
1800 sub find_filter {
1801     my $self = shift;
1802     my $needle = shift;;
1803     return undef unless ($needle);
1804
1805     my $filter = $self->collapse_filters($needle);
1806
1807     warn "find_filter($needle) => " . 
1808         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1809         if $self->QueryParser->debug;
1810
1811     return $filter ? ($filter) : ();
1812 }
1813
1814 sub find_modifier {
1815     my $self = shift;
1816     my $needle = shift;;
1817     return undef unless ($needle);
1818     return grep { $_->name eq $needle } @{ $self->modifiers };
1819 }
1820
1821 sub new_modifier {
1822     my $self = shift;
1823     my $pkg = ref($self) || $self;
1824     my $name = shift;
1825
1826     my $node = do{$pkg.'::modifier'}->new( $name );
1827     $self->add_modifier( $node );
1828
1829     return $node;
1830 }
1831
1832 sub classed_node {
1833     my $self = shift;
1834     my $requested_class = shift;
1835
1836     my $node;
1837     for my $n (@{$self->{query}}) {
1838         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1839         if ($n->requested_class eq $requested_class) {
1840             $node = $n;
1841             last;
1842         }
1843     }
1844
1845     if (!$node) {
1846         $node = $self->new_node;
1847         $node->requested_class( $requested_class );
1848     }
1849
1850     return $node;
1851 }
1852
1853 sub remove_last_node {
1854     my $self = shift;
1855     my $requested_class = shift;
1856
1857     my $old = pop(@{$self->query_nodes});
1858     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1859
1860     return $old;
1861 }
1862
1863 sub query_nodes {
1864     my $self = shift;
1865     return $self->{query};
1866 }
1867
1868 sub floating {
1869     my $self = shift;
1870     my $f = shift;
1871     $self->{floating} = $f if (defined $f);
1872     return $self->{floating};
1873 }
1874
1875 sub explicit {
1876     my $self = shift;
1877     my $f = shift;
1878     $self->{explicit} = $f if (defined $f);
1879     return $self->{explicit};
1880 }
1881
1882 sub add_node {
1883     my $self = shift;
1884     my $node = shift;
1885
1886     $self->{query} ||= [];
1887     if ($node) {
1888         push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1889         push(@{$self->{query}}, $node);
1890     }
1891
1892     return $self;
1893 }
1894
1895 sub top_plan {
1896     my $self = shift;
1897
1898     return $self->{level} ? 0 : 1;
1899 }
1900
1901 sub plan_level {
1902     my $self = shift;
1903     my $level = shift;
1904
1905     if (defined $level) {
1906         $self->{level} = $level;
1907         for (@{$self->query_nodes}) {
1908             $_->plan_level($level + 1) if (ref and $_->isa('QueryParser::query_plan'));
1909         }
1910     }
1911             
1912     return $self->{level};
1913 }
1914
1915 sub joiner {
1916     my $self = shift;
1917     my $joiner = shift;
1918
1919     $self->{joiner} = $joiner if ($joiner);
1920     return $self->{joiner};
1921 }
1922
1923 sub modifiers {
1924     my $self = shift;
1925     $self->{modifiers} ||= [];
1926     return $self->{modifiers};
1927 }
1928
1929 sub add_modifier {
1930     my $self = shift;
1931     my $modifier = shift;
1932
1933     $self->{modifiers} ||= [];
1934     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1935
1936     push(@{$self->{modifiers}}, $modifier);
1937
1938     return $self;
1939 }
1940
1941 sub facets {
1942     my $self = shift;
1943     $self->{facets} ||= [];
1944     return $self->{facets};
1945 }
1946
1947 sub add_facet {
1948     my $self = shift;
1949     my $facet = shift;
1950
1951     $self->{facets} ||= [];
1952     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1953
1954     push(@{$self->{facets}}, $facet);
1955
1956     return $self;
1957 }
1958
1959 sub filters {
1960     my $self = shift;
1961     $self->{filters} ||= [];
1962     return $self->{filters};
1963 }
1964
1965 sub add_filter {
1966     my $self = shift;
1967     my $filter = shift;
1968
1969     $self->{filters} ||= [];
1970
1971     push(@{$self->{filters}}, $filter);
1972
1973     return $self;
1974 }
1975
1976 sub negate {
1977     my $self = shift;
1978     my $negate = shift;
1979
1980     $self->{negate} = $negate if (defined $negate);
1981
1982     return $self->{negate};
1983 }
1984
1985 # %opts supports two options at this time:
1986 #   no_phrases :
1987 #       If true, do not do anything to the phrases
1988 #       fields on any discovered nodes.
1989 #   with_config :
1990 #       If true, also return the query parser config as part of the blob.
1991 #       This will get set back to 0 before recursion to avoid repetition.
1992 sub to_abstract_query {
1993     my $self = shift;
1994     my %opts = @_;
1995
1996     my $pkg = ref $self->QueryParser || $self->QueryParser;
1997
1998     my $abstract_query = {
1999         type => "query_plan",
2000         floating => $self->floating,
2001         level => $self->plan_level,
2002         filters => [map { $_->to_abstract_query } @{$self->filters}],
2003         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}],
2004         negate => $self->negate
2005     };
2006
2007     if ($opts{with_config}) {
2008         $opts{with_config} = 0;
2009         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
2010     }
2011
2012     my $kids = [];
2013
2014     my $prev_was_joiner = 0;
2015     for my $qnode (@{$self->query_nodes}) {
2016         # Remember: qnode can be a joiner string, a node, or another query_plan
2017
2018         if (QueryParser::_util::is_joiner($qnode)) {
2019             unless ($prev_was_joiner) {
2020                 if ($abstract_query->{children}) {
2021                     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2022                     next if $open_joiner eq $qnode;
2023
2024                     my $oldroot = $abstract_query->{children};
2025                     $kids = [$oldroot];
2026                     $abstract_query->{children} = {$qnode => $kids};
2027                 } else {
2028                     $abstract_query->{children} = {$qnode => $kids};
2029                 }
2030             }
2031             $prev_was_joiner = 1;
2032         } elsif ($qnode) {
2033             if (my $next_kid = $qnode->to_abstract_query(%opts)) {
2034                 push @$kids, $qnode->to_abstract_query(%opts);
2035                 $prev_was_joiner = 0;
2036             }
2037         }
2038     }
2039
2040     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2041     $$abstract_query{additional_data} = $self->{abstract_data}
2042         if (keys(%{$self->{abstract_data}}));
2043
2044     return $abstract_query;
2045 }
2046
2047
2048 #-------------------------------
2049 package QueryParser::query_plan::node;
2050 use Data::Dumper;
2051 $Data::Dumper::Indent = 0;
2052
2053 sub effective_joiner {
2054     my $node = shift;
2055
2056     my @nodelist = @{$node->query_atoms};
2057     return $node->plan->joiner if (@nodelist == 1);
2058
2059     # gather the joiners
2060     my %joiners = ( '&' => 0, '|' => 0 );
2061     while (my $n = shift(@nodelist)) {
2062         next if ref($n); # only look at joiners
2063         $joiners{$n}++;
2064     }
2065
2066     if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # no mix of joiners
2067         return '|' if ($joiners{'|'});
2068         return '&';
2069     }
2070
2071     return undef;
2072 }
2073
2074 sub new {
2075     my $pkg = shift;
2076     $pkg = ref($pkg) || $pkg;
2077     my %args = @_;
2078
2079     return bless \%args => $pkg;
2080 }
2081
2082 sub new_atom {
2083     my $self = shift;
2084     my $pkg = ref($self) || $self;
2085     return do{$pkg.'::atom'}->new( @_ );
2086 }
2087
2088 sub requested_class { # also split into classname, fields and alias
2089     my $self = shift;
2090     my $class = shift;
2091
2092     if ($class) {
2093         my @afields;
2094         my (undef, $alias) = split '#', $class;
2095         if ($alias) {
2096             $class =~ s/#[^|]+//;
2097             ($alias, @afields) = split '\|', $alias;
2098         }
2099
2100         my @fields = @afields;
2101         my ($class_part, @field_parts) = split '\|', $class;
2102         for my $f (@field_parts) {
2103              push(@fields, $f) unless (grep { $f eq $_ } @fields);
2104         }
2105
2106         $class_part ||= $class;
2107
2108         $self->{requested_class} = $class;
2109         $self->{alias} = $alias if $alias;
2110         $self->{alias_fields} = \@afields if $alias;
2111         $self->{classname} = $class_part;
2112         $self->{fields} = \@fields;
2113     }
2114
2115     return $self->{requested_class};
2116 }
2117
2118 sub plan {
2119     my $self = shift;
2120     my $plan = shift;
2121
2122     $self->{plan} = $plan if ($plan);
2123     return $self->{plan};
2124 }
2125
2126 sub alias {
2127     my $self = shift;
2128     my $alias = shift;
2129
2130     $self->{alias} = $alias if ($alias);
2131     return $self->{alias};
2132 }
2133
2134 sub alias_fields {
2135     my $self = shift;
2136     my $alias = shift;
2137
2138     $self->{alias_fields} = $alias if ($alias);
2139     return $self->{alias_fields};
2140 }
2141
2142 sub classname {
2143     my $self = shift;
2144     my $class = shift;
2145
2146     $self->{classname} = $class if ($class);
2147     return $self->{classname};
2148 }
2149
2150 sub fields {
2151     my $self = shift;
2152     my @fields = @_;
2153
2154     $self->{fields} ||= [];
2155     $self->{fields} = \@fields if (@fields);
2156     return $self->{fields};
2157 }
2158
2159 sub phrases {
2160     my $self = shift;
2161     my @phrases = @_;
2162
2163     $self->{phrases} ||= [];
2164     $self->{phrases} = \@phrases if (@phrases);
2165     return $self->{phrases};
2166 }
2167
2168 sub add_phrase {
2169     my $self = shift;
2170     my $phrase = shift;
2171
2172     push(@{$self->phrases}, $phrase);
2173
2174     return $self;
2175 }
2176
2177 sub negate {
2178     my $self = shift;
2179     my $negate = shift;
2180
2181     $self->{negate} = $negate if (defined $negate);
2182
2183     return $self->{negate};
2184 }
2185
2186 sub query_atoms {
2187     my $self = shift;
2188     my @query_atoms = @_;
2189
2190     $self->{query_atoms} ||= [];
2191     $self->{query_atoms} = \@query_atoms if (@query_atoms);
2192     return $self->{query_atoms};
2193 }
2194
2195 sub add_fts_atom {
2196     my $self = shift;
2197     my $atom = shift;
2198
2199     if (!ref($atom)) {
2200         my $content = $atom;
2201         my @parts = @_;
2202
2203         $atom = $self->new_atom( content => $content, node => $self, @parts );
2204     }
2205
2206     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2207     push(@{$self->query_atoms}, $atom);
2208
2209     return $self;
2210 }
2211
2212 sub add_dummy_atom {
2213     my $self = shift;
2214     my @parts = @_;
2215
2216     my $atom = $self->new_atom( node => $self, @parts, dummy => 1 );
2217
2218     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2219     push(@{$self->query_atoms}, $atom);
2220
2221     return $self;
2222 }
2223
2224 # This will find up to one occurence of @$short_list within @$long_list, and
2225 # replace it with the single atom $replacement.
2226 sub replace_phrase_in_abstract_query {
2227     my ($self, $short_list, $long_list, $replacement) = @_;
2228
2229     my $success = 0;
2230     my @already = ();
2231     my $goal = scalar @$short_list;
2232
2233     for (my $i = 0; $i < scalar (@$long_list); $i++) {
2234         my $right = $long_list->[$i];
2235
2236         if (QueryParser::_util::compare_abstract_atoms(
2237             $short_list->[scalar @already], $right
2238         )) {
2239             push @already, $i;
2240         } elsif (scalar @already) {
2241             @already = ();
2242             next;
2243         }
2244
2245         if (scalar @already == $goal) {
2246             splice @$long_list, $already[0], scalar(@already), $replacement;
2247             $success = 1;
2248             last;
2249         }
2250     }
2251
2252     return $success;
2253 }
2254
2255 sub to_abstract_query {
2256     my $self = shift;
2257     my %opts = @_;
2258
2259     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
2260
2261     my $abstract_query = {
2262         "type" => "node",
2263         "alias" => $self->alias,
2264         "alias_fields" => $self->alias_fields,
2265         "class" => $self->classname,
2266         "fields" => $self->fields
2267     };
2268
2269     $self->abstract_node_additions($abstract_query)
2270         if ($self->can('abstract_node_additions'));
2271
2272     my $kids = [];
2273
2274     my $prev_was_joiner = 0;
2275     for my $qatom (grep {!ref($_) or !$_->dummy} @{$self->query_atoms}) {
2276         if (QueryParser::_util::is_joiner($qatom)) {
2277             unless ($prev_was_joiner) {
2278                 if ($abstract_query->{children}) {
2279                     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2280                     next if $open_joiner eq $qatom;
2281
2282                     my $oldroot = $abstract_query->{children};
2283                     $kids = [$oldroot];
2284                     $abstract_query->{children} = {$qatom => $kids};
2285                 } else {
2286                     $abstract_query->{children} = {$qatom => $kids};
2287                 }
2288             }
2289             $prev_was_joiner = 1;
2290         } else {
2291             push @$kids, $qatom->to_abstract_query;
2292             $prev_was_joiner = 0;
2293         }
2294     }
2295
2296     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2297
2298     if ($self->phrases and @{$self->phrases} and not $opts{no_phrases}) {
2299         my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2300         if ($open_joiner ne '&') {
2301             my $oldroot = $abstract_query->{children};
2302             $kids = [$oldroot];
2303             $abstract_query->{children} = {'&' => $kids};
2304         }
2305
2306         for my $phrase (@{$self->phrases}) {
2307             # Phrases appear duplication in a real QP tree, and we don't want
2308             # that duplication in our abstract query.  So for all our phrases,
2309             # break them into atoms as QP would, and remove any matching
2310             # sequences of atoms from our abstract query.
2311
2312             my $tmp_prefix = '';
2313             $tmp_prefix = $QueryParser::parser_config{$pkg}{operators}{disallowed} if ($self->{negate});
2314
2315             my $tmptree = $self->{plan}->{QueryParser}->new(query => $phrase)->parse->parse_tree;
2316             if ($tmptree) {
2317                 # For a well-behaved phrase, we should now have only one node
2318                 # in the $tmptree query plan, and that node should have an
2319                 # orderly list of atoms and joiners.
2320
2321                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2322                     my $tmplist;
2323
2324                     eval {
2325                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2326                             no_phrases => 1
2327                         )->{children}->{'&'};
2328                     };
2329                     next if $@ or !ref($tmplist);
2330
2331                     $$tmplist[0]{prefix} = $tmp_prefix.'"';
2332                     $$tmplist[-1]{suffix} = '"';
2333                     push @{$abstract_query->{children}->{'&'}}, @$tmplist;
2334                 }
2335             }
2336         }
2337     }
2338
2339     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2340
2341     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2342     return undef unless @{$abstract_query->{children}->{$open_joiner}};
2343
2344     return $abstract_query;
2345 }
2346
2347 #-------------------------------
2348 package QueryParser::query_plan::node::atom;
2349
2350 sub new {
2351     my $pkg = shift;
2352     $pkg = ref($pkg) || $pkg;
2353     my %args = @_;
2354
2355     return bless \%args => $pkg;
2356 }
2357
2358 sub node {
2359     my $self = shift;
2360     return undef unless (ref $self);
2361     return $self->{node};
2362 }
2363
2364 sub content {
2365     my $self = shift;
2366     return undef unless (ref $self);
2367     return $self->{content};
2368 }
2369
2370 sub prefix {
2371     my $self = shift;
2372     return undef unless (ref $self);
2373     return $self->{prefix};
2374 }
2375
2376 sub suffix {
2377     my $self = shift;
2378     return undef unless (ref $self);
2379     return $self->{suffix};
2380 }
2381
2382 sub explicit_start {
2383     my $self = shift;
2384     my $explicit_start = shift;
2385
2386     $self->{explicit_start} = $explicit_start if (defined $explicit_start);
2387
2388     return $self->{explicit_start};
2389 }
2390
2391 sub dummy {
2392     my $self = shift;
2393     my $dummy = shift;
2394
2395     $self->{dummy} = $dummy if (defined $dummy);
2396
2397     return $self->{dummy};
2398 }
2399
2400 sub explicit_end {
2401     my $self = shift;
2402     my $explicit_end = shift;
2403
2404     $self->{explicit_end} = $explicit_end if (defined $explicit_end);
2405
2406     return $self->{explicit_end};
2407 }
2408
2409 sub to_abstract_query {
2410     my ($self) = @_;
2411     
2412     return {
2413         (map { $_ => $self->$_ } qw/dummy prefix suffix content explicit_start explicit_end/),
2414         "type" => "atom"
2415     };
2416 }
2417 #-------------------------------
2418 package QueryParser::query_plan::filter;
2419
2420 sub new {
2421     my $pkg = shift;
2422     $pkg = ref($pkg) || $pkg;
2423     my %args = @_;
2424
2425     return bless \%args => $pkg;
2426 }
2427
2428 sub plan {
2429     my $self = shift;
2430     return $self->{plan};
2431 }
2432
2433 sub name {
2434     my $self = shift;
2435     return $self->{name};
2436 }
2437
2438 sub negate {
2439     my $self = shift;
2440     return $self->{negate};
2441 }
2442
2443 sub args {
2444     my $self = shift;
2445     return $self->{args};
2446 }
2447
2448 sub to_abstract_query {
2449     my ($self) = @_;
2450     
2451     return {
2452         map { $_ => $self->$_ } qw/name negate args/
2453     };
2454 }
2455
2456 #-------------------------------
2457 package QueryParser::query_plan::facet;
2458
2459 sub new {
2460     my $pkg = shift;
2461     $pkg = ref($pkg) || $pkg;
2462     my %args = @_;
2463
2464     return bless \%args => $pkg;
2465 }
2466
2467 sub plan {
2468     my $self = shift;
2469     return $self->{plan};
2470 }
2471
2472 sub name {
2473     my $self = shift;
2474     return $self->{name};
2475 }
2476
2477 sub negate {
2478     my $self = shift;
2479     return $self->{negate};
2480 }
2481
2482 sub values {
2483     my $self = shift;
2484     return $self->{'values'};
2485 }
2486
2487 sub to_abstract_query {
2488     my ($self) = @_;
2489
2490     return {
2491         (map { $_ => $self->$_ } qw/name negate values/),
2492         "type" => "facet"
2493     };
2494 }
2495
2496 #-------------------------------
2497 package QueryParser::query_plan::modifier;
2498
2499 sub new {
2500     my $pkg = shift;
2501     $pkg = ref($pkg) || $pkg;
2502     my $modifier = shift;
2503     my $negate = shift;
2504
2505     return bless { name => $modifier, negate => $negate } => $pkg;
2506 }
2507
2508 sub name {
2509     my $self = shift;
2510     return $self->{name};
2511 }
2512
2513 sub negate {
2514     my $self = shift;
2515     return $self->{negate};
2516 }
2517
2518 sub to_abstract_query {
2519     my ($self) = @_;
2520     
2521     return $self->name;
2522 }
2523 1;
2524