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