]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
0be69cb06ee59a8e16c2f854458ec1d86b887056
[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 our %parser_config = (
7     QueryParser => {
8         filters => [],
9         modifiers => [],
10         operators => { 
11             'and' => '&&',
12             'or' => '||',
13             float_start => '{{',
14             float_end => '}}',
15             group_start => '(',
16             group_end => ')',
17             required => '+',
18             disallowed => '-',
19             modifier => '#'
20         }
21     }
22 );
23
24 sub canonicalize {
25     my $self = shift;
26     return QueryParser::Canonicalize::abstract_query2str_impl(
27         $self->parse_tree->to_abstract_query(@_)
28     );
29 }
30
31
32 sub facet_class_count {
33     my $self = shift;
34     return @{$self->facet_classes};
35 }
36
37 sub search_class_count {
38     my $self = shift;
39     return @{$self->search_classes};
40 }
41
42 sub filter_count {
43     my $self = shift;
44     return @{$self->filters};
45 }
46
47 sub modifier_count {
48     my $self = shift;
49     return @{$self->modifiers};
50 }
51
52 sub custom_data {
53     my $class = shift;
54     $class = ref($class) || $class;
55
56     $parser_config{$class}{custom_data} ||= {};
57     return $parser_config{$class}{custom_data};
58 }
59
60 sub operators {
61     my $class = shift;
62     $class = ref($class) || $class;
63
64     $parser_config{$class}{operators} ||= {};
65     return $parser_config{$class}{operators};
66 }
67
68 sub allow_nested_modifiers {
69     my $class = shift;
70     my $v = shift;
71     $class = ref($class) || $class;
72
73     $parser_config{$class}{allow_nested_modifiers} = $v if (defined $v);
74     return $parser_config{$class}{allow_nested_modifiers};
75 }
76
77 sub filters {
78     my $class = shift;
79     $class = ref($class) || $class;
80
81     $parser_config{$class}{filters} ||= [];
82     return $parser_config{$class}{filters};
83 }
84
85 sub filter_callbacks {
86     my $class = shift;
87     $class = ref($class) || $class;
88
89     $parser_config{$class}{filter_callbacks} ||= {};
90     return $parser_config{$class}{filter_callbacks};
91 }
92
93 sub modifiers {
94     my $class = shift;
95     $class = ref($class) || $class;
96
97     $parser_config{$class}{modifiers} ||= [];
98     return $parser_config{$class}{modifiers};
99 }
100
101 sub new {
102     my $class = shift;
103     $class = ref($class) || $class;
104
105     my %opts = @_;
106
107     my $self = bless {} => $class;
108
109     for my $o (keys %{QueryParser->operators}) {
110         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
111     }
112
113     for my $opt ( keys %opts) {
114         $self->$opt( $opts{$opt} ) if ($self->can($opt));
115     }
116
117     return $self;
118 }
119
120 sub new_plan {
121     my $self = shift;
122     my $pkg = ref($self) || $self;
123     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
124 }
125
126 sub add_search_filter {
127     my $pkg = shift;
128     $pkg = ref($pkg) || $pkg;
129     my $filter = shift;
130     my $callback = shift;
131
132     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
133     push @{$pkg->filters}, $filter;
134     $pkg->filter_callbacks->{$filter} = $callback if ($callback);
135     return $filter;
136 }
137
138 sub add_search_modifier {
139     my $pkg = shift;
140     $pkg = ref($pkg) || $pkg;
141     my $modifier = shift;
142
143     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
144     push @{$pkg->modifiers}, $modifier;
145     return $modifier;
146 }
147
148 sub add_facet_class {
149     my $pkg = shift;
150     $pkg = ref($pkg) || $pkg;
151     my $class = shift;
152
153     return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
154
155     push @{$pkg->facet_classes}, $class;
156     $pkg->facet_fields->{$class} = [];
157
158     return $class;
159 }
160
161 sub add_search_class {
162     my $pkg = shift;
163     $pkg = ref($pkg) || $pkg;
164     my $class = shift;
165
166     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
167
168     push @{$pkg->search_classes}, $class;
169     $pkg->search_fields->{$class} = [];
170     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
171
172     return $class;
173 }
174
175 sub operator {
176     my $class = shift;
177     $class = ref($class) || $class;
178     my $opname = shift;
179     my $op = shift;
180
181     return undef unless ($opname);
182
183     $parser_config{$class}{operators} ||= {};
184     $parser_config{$class}{operators}{$opname} = $op if ($op);
185
186     return $parser_config{$class}{operators}{$opname};
187 }
188
189 sub facet_classes {
190     my $class = shift;
191     $class = ref($class) || $class;
192     my $classes = shift;
193
194     $parser_config{$class}{facet_classes} ||= [];
195     $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
196     return $parser_config{$class}{facet_classes};
197 }
198
199 sub search_classes {
200     my $class = shift;
201     $class = ref($class) || $class;
202     my $classes = shift;
203
204     $parser_config{$class}{classes} ||= [];
205     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
206     return $parser_config{$class}{classes};
207 }
208
209 sub add_query_normalizer {
210     my $pkg = shift;
211     $pkg = ref($pkg) || $pkg;
212     my $class = shift;
213     my $field = shift;
214     my $func = shift;
215     my $params = shift || [];
216
217     # do not add if function AND params are identical to existing member
218     return $func if (grep {
219         $_->{function} eq $func and 
220         OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
221     } @{$pkg->query_normalizers->{$class}->{$field}});
222
223     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
224
225     return $func;
226 }
227
228 sub query_normalizers {
229     my $pkg = shift;
230     $pkg = ref($pkg) || $pkg;
231
232     my $class = shift;
233     my $field = shift;
234
235     $parser_config{$pkg}{normalizers} ||= {};
236     if ($class) {
237         if ($field) {
238             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
239             return $parser_config{$pkg}{normalizers}{$class}{$field};
240         } else {
241             return $parser_config{$pkg}{normalizers}{$class};
242         }
243     }
244
245     return $parser_config{$pkg}{normalizers};
246 }
247
248 sub add_filter_normalizer {
249     my $pkg = shift;
250     $pkg = ref($pkg) || $pkg;
251     my $filter = shift;
252     my $func = shift;
253     my $params = shift || [];
254
255     return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
256
257     push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
258
259     return $func;
260 }
261
262 sub filter_normalizers {
263     my $pkg = shift;
264     $pkg = ref($pkg) || $pkg;
265
266     my $filter = shift;
267
268     $parser_config{$pkg}{filter_normalizers} ||= {};
269     if ($filter) {
270         $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
271         return $parser_config{$pkg}{filter_normalizers}{$filter};
272     }
273
274     return $parser_config{$pkg}{filter_normalizers};
275 }
276
277 sub default_search_class {
278     my $pkg = shift;
279     $pkg = ref($pkg) || $pkg;
280     my $class = shift;
281     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
282
283     return $QueryParser::parser_config{$pkg}{default_class};
284 }
285
286 sub remove_facet_class {
287     my $pkg = shift;
288     $pkg = ref($pkg) || $pkg;
289     my $class = shift;
290
291     return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
292
293     $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
294     delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
295
296     return $class;
297 }
298
299 sub remove_search_class {
300     my $pkg = shift;
301     $pkg = ref($pkg) || $pkg;
302     my $class = shift;
303
304     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
305
306     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
307     delete $QueryParser::parser_config{$pkg}{fields}{$class};
308
309     return $class;
310 }
311
312 sub add_facet_field {
313     my $pkg = shift;
314     $pkg = ref($pkg) || $pkg;
315     my $class = shift;
316     my $field = shift;
317
318     $pkg->add_facet_class( $class );
319
320     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
321
322     push @{$pkg->facet_fields->{$class}}, $field;
323
324     return { $class => $field };
325 }
326
327 sub facet_fields {
328     my $class = shift;
329     $class = ref($class) || $class;
330
331     $parser_config{$class}{facet_fields} ||= {};
332     return $parser_config{$class}{facet_fields};
333 }
334
335 sub add_search_field {
336     my $pkg = shift;
337     $pkg = ref($pkg) || $pkg;
338     my $class = shift;
339     my $field = shift;
340
341     $pkg->add_search_class( $class );
342
343     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
344
345     push @{$pkg->search_fields->{$class}}, $field;
346
347     return { $class => $field };
348 }
349
350 sub search_fields {
351     my $class = shift;
352     $class = ref($class) || $class;
353
354     $parser_config{$class}{fields} ||= {};
355     return $parser_config{$class}{fields};
356 }
357
358 sub add_search_class_alias {
359     my $pkg = shift;
360     $pkg = ref($pkg) || $pkg;
361     my $class = shift;
362     my $alias = shift;
363
364     $pkg->add_search_class( $class );
365
366     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
367
368     push @{$pkg->search_class_aliases->{$class}}, $alias;
369
370     return { $class => $alias };
371 }
372
373 sub search_class_aliases {
374     my $class = shift;
375     $class = ref($class) || $class;
376
377     $parser_config{$class}{class_map} ||= {};
378     return $parser_config{$class}{class_map};
379 }
380
381 sub add_search_field_alias {
382     my $pkg = shift;
383     $pkg = ref($pkg) || $pkg;
384     my $class = shift;
385     my $field = shift;
386     my $alias = shift;
387
388     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
389
390     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
391
392     return { $class => { $field => $alias } };
393 }
394
395 sub search_field_aliases {
396     my $class = shift;
397     $class = ref($class) || $class;
398
399     $parser_config{$class}{field_alias_map} ||= {};
400     return $parser_config{$class}{field_alias_map};
401 }
402
403 sub remove_facet_field {
404     my $pkg = shift;
405     $pkg = ref($pkg) || $pkg;
406     my $class = shift;
407     my $field = shift;
408
409     return { $class => $field }  if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
410
411     $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
412
413     return { $class => $field };
414 }
415
416 sub remove_search_field {
417     my $pkg = shift;
418     $pkg = ref($pkg) || $pkg;
419     my $class = shift;
420     my $field = shift;
421
422     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
423
424     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
425
426     return { $class => $field };
427 }
428
429 sub remove_search_field_alias {
430     my $pkg = shift;
431     $pkg = ref($pkg) || $pkg;
432     my $class = shift;
433     my $field = shift;
434     my $alias = shift;
435
436     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
437
438     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
439
440     return { $class => { $field => $alias } };
441 }
442
443 sub remove_search_class_alias {
444     my $pkg = shift;
445     $pkg = ref($pkg) || $pkg;
446     my $class = shift;
447     my $alias = shift;
448
449     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
450
451     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
452
453     return { $class => $alias };
454 }
455
456 sub debug {
457     my $self = shift;
458     my $q = shift;
459     $self->{_debug} = $q if (defined $q);
460     return $self->{_debug};
461 }
462
463 sub query {
464     my $self = shift;
465     my $q = shift;
466     $self->{_query} = " $q " if (defined $q);
467     return $self->{_query};
468 }
469
470 sub parse_tree {
471     my $self = shift;
472     my $q = shift;
473     $self->{_parse_tree} = $q if (defined $q);
474     return $self->{_parse_tree};
475 }
476
477 sub floating_plan {
478     my $self = shift;
479     my $q = shift;
480     $self->{_top} = $q if (defined $q);
481     return $self->{_top};
482 }
483
484 sub parse {
485     my $self = shift;
486     my $pkg = ref($self) || $self;
487     warn " ** parse package is $pkg\n" if $self->debug;
488 #    $self->parse_tree(
489 #        $self->decompose(
490 #            $self->query( shift() )
491 #        )
492 #    );
493
494     $self->decompose( $self->query( shift() ) );
495
496     if ($self->floating_plan) {
497         $self->floating_plan->add_node( $self->parse_tree );
498         $self->parse_tree( $self->floating_plan );
499     }
500     return $self;
501 }
502
503 sub decompose {
504     my $self = shift;
505     my $pkg = ref($self) || $self;
506
507     warn " ** decompose package is $pkg\n" if $self->debug;
508
509     $_ = shift;
510     my $current_class = shift || $self->default_search_class;
511
512     my $recursing = shift || 0;
513     my $phrase_helper = shift || 0;
514
515     # Build the search class+field uber-regexp
516     my $search_class_re = '^\s*(';
517     my $first_class = 1;
518
519     my %seen_classes;
520     for my $class ( keys %{$pkg->search_field_aliases} ) {
521         warn " *** ... Looking for search fields in $class\n" if $self->debug;
522
523         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
524             warn " *** ... Looking for aliases of $field\n" if $self->debug;
525
526             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
527                 my $aliasr = qr/$alias/;
528                 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
529                 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
530                 warn " *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
531             }
532         }
533
534         $search_class_re .= '|' unless ($first_class);
535         $first_class = 0;
536         $search_class_re .= $class . '(?:[|#][^:|]+)*';
537         $seen_classes{$class} = 1;
538     }
539
540     for my $class ( keys %{$pkg->search_class_aliases} ) {
541
542         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
543             my $aliasr = qr/$alias/;
544             s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
545             s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
546             warn " *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
547         }
548
549         if (!$seen_classes{$class}) {
550             $search_class_re .= '|' unless ($first_class);
551             $first_class = 0;
552
553             $search_class_re .= $class . '(?:[|#][^:|]+)*';
554             $seen_classes{$class} = 1;
555         }
556     }
557     $search_class_re .= '):';
558
559     warn " ** Rewritten query: $_\n" if $self->debug;
560     warn " ** Search class RE: $search_class_re\n" if $self->debug;
561
562     my $required_re = $pkg->operator('required');
563     $required_re = qr/\Q$required_re\E/;
564
565     my $disallowed_re = $pkg->operator('disallowed');
566     $disallowed_re = qr/\Q$disallowed_re\E/;
567
568     my $and_re = $pkg->operator('and');
569     $and_re = qr/^\s*\Q$and_re\E/;
570
571     my $or_re = $pkg->operator('or');
572     $or_re = qr/^\s*\Q$or_re\E/;
573
574     my $group_start = $pkg->operator('group_start');
575     my $group_start_re = qr/^\s*\Q$group_start\E/;
576
577     my $group_end = $pkg->operator('group_end');
578     my $group_end_re = qr/^\s*\Q$group_end\E/;
579
580     my $float_start = $pkg->operator('float_start');
581     my $float_start_re = qr/^\s*\Q$float_start\E/;
582
583     my $float_end = $pkg->operator('float_end');
584     my $float_end_re = qr/^\s*\Q$float_end\E/;
585
586     my $modifier_tag_re = $pkg->operator('modifier');
587     $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
588
589
590     # Build the filter and modifier uber-regexps
591     my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
592     warn " ** Facet RE: $facet_re\n" if $self->debug;
593
594     my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
595     my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
596
597     my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
598     my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
599
600     my $struct = shift || $self->new_plan( level => $recursing );
601     $self->parse_tree( $struct ) if (!$self->parse_tree);
602
603     my $remainder = '';
604
605     my $last_type = '';
606     while (!$remainder) {
607         if (/^\s*$/) { # end of an explicit group
608             last;
609         } elsif (/$float_end_re/) { # end of an explicit group
610             warn "Encountered explicit float end\n" if $self->debug;
611
612             $remainder = $';
613             $_ = '';
614
615             $last_type = '';
616         } elsif (/$group_end_re/) { # end of an explicit group
617             warn "Encountered explicit group end\n" if $self->debug;
618
619             $_ = $';
620             $remainder = $struct->top_plan ? '' : $';
621
622             $last_type = '';
623         } elsif ($self->filter_count && /$filter_re/) { # found a filter
624             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
625
626             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
627             $_ = $';
628
629             my $filter = $2;
630             my $params = [ split '[,]+', $3 ];
631
632             if ($pkg->filter_callbacks->{$filter}) {
633                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
634                 $_ = "$replacement $_" if ($replacement);
635             } else {
636                 $struct->new_filter( $filter => $params, $negate );
637             }
638
639
640             $last_type = '';
641         } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
642             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
643
644             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
645             $_ = $';
646
647             my $filter = $2;
648             my $params = [ split '[,]+', $3 ];
649
650             if ($pkg->filter_callbacks->{$filter}) {
651                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
652                 $_ = "$replacement $_" if ($replacement);
653             } else {
654                 $struct->new_filter( $filter => $params, $negate );
655             }
656
657             $last_type = '';
658         } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
659             warn "Encountered search modifier: $1\n" if $self->debug;
660
661             $_ = $';
662             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
663                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
664             } else {
665                 $struct->new_modifier($1);
666             }
667
668             $last_type = '';
669         } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
670             warn "Encountered search modifier: $1\n" if $self->debug;
671
672             my $mod = $1;
673
674             $_ = $';
675             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
676                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
677             } elsif ($2 =~ /^[ty1]/i) {
678                 $struct->new_modifier($mod);
679             }
680
681             $last_type = '';
682         } elsif (/$float_start_re/) { # start of an explicit float
683             warn "Encountered explicit float start\n" if $self->debug;
684
685             $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
686             # pass the floating_plan struct to be modified by the float'ed chunk
687             my ($floating_plan, $subremainder) = $self->new->decompose( $', undef, undef, undef,  $self->floating_plan);
688             $_ = $subremainder;
689
690             $last_type = '';
691         } elsif (/$group_start_re/) { # start of an explicit group
692             warn "Encountered explicit group start\n" if $self->debug;
693
694             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
695             $struct->add_node( $substruct ) if ($substruct);
696             $_ = $subremainder;
697
698             $last_type = '';
699         } elsif (/$and_re/) { # ANDed expression
700             $_ = $';
701             next if ($last_type eq 'AND');
702             next if ($last_type eq 'OR');
703             warn "Encountered AND\n" if $self->debug;
704
705             my $LHS = $struct;
706             my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
707             $_ = $subremainder;
708
709             $struct = $self->new_plan( level => $recursing, joiner => '&', floating => $LHS->floating );
710             if ($LHS->floating) {
711                 $self->floating_plan($struct);
712                 $LHS->floating(0);
713             }
714
715             $struct->add_node($_) for ($LHS, $RHS);
716
717             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
718
719             $last_type = 'AND';
720         } elsif (/$or_re/) { # ORed expression
721             $_ = $';
722             next if ($last_type eq 'AND');
723             next if ($last_type eq 'OR');
724             warn "Encountered OR\n" if $self->debug;
725
726             my $LHS = $struct;
727             my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
728             $_ = $subremainder;
729
730             $struct = $self->new_plan( level => $recursing, joiner => '|' );
731             $struct->add_node($_) for ($LHS, $RHS);
732
733             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
734
735             $last_type = 'OR';
736         } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
737             warn "Encountered facet: $1$2 => $3\n" if $self->debug;
738
739             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
740             my $facet = $2;
741             my $facet_value = [ split '\s*#\s*', $3 ];
742             $struct->new_facet( $facet => $facet_value, $negate );
743             $_ = $';
744
745             $last_type = '';
746         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
747
748             if ($last_type eq 'CLASS') {
749                 $struct->remove_last_node( $current_class );
750                 warn "Encountered class change with no searches!\n" if $self->debug;
751             }
752
753             warn "Encountered class change: $1\n" if $self->debug;
754
755             $current_class = $struct->classed_node( $1 )->requested_class();
756             $_ = $';
757
758             $last_type = 'CLASS';
759         } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
760             warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
761
762             my $req_ness = $1 || '';
763             my $phrase = $2;
764
765             if (!$phrase_helper) {
766                 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
767                 my $after = $';
768                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
769                 $struct->add_node( $substruct ) if ($substruct);
770                 $_ = $after;
771             } else {
772                 warn "Directly parsing the phrase subquery\n" if $self->debug;
773                 $struct->joiner( '&' );
774
775                 my $class_node = $struct->classed_node($current_class);
776
777                 if ($req_ness eq $pkg->operator('disallowed')) {
778                     $class_node->add_dummy_atom( node => $class_node );
779                     $class_node->add_unphrase( $phrase );
780                     $phrase = '';
781                     #$phrase =~ s/(^|\s)\b/$1-/g;
782                 } else { 
783                     $class_node->add_phrase( $phrase );
784                 }
785                 $_ = $phrase . $';
786
787             }
788
789             $last_type = '';
790
791 #        } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
792 #            warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
793 #
794 #            my $phrase = $1;
795 #
796 #            my $class_node = $struct->classed_node($current_class);
797 #            $class_node->add_phrase( $phrase );
798 #            $_ = $phrase . $';
799 #            $struct->joiner( '&' );
800 #
801 #            $last_type = '';
802         } elsif (/^\s*([^${group_end}${float_end}\s]+)/o) { # atom
803             warn "Encountered atom: $1\n" if $self->debug;
804             warn "Remainder: $'\n" if $self->debug;
805
806             my $atom = $1;
807             my $after = $';
808
809             $_ = $after;
810             $last_type = '';
811
812             my $class_node = $struct->classed_node($current_class);
813
814             my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
815             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
816
817             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
818 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
819 #                $class_node->add_unphrase( $atom ) if ($prefix eq '!');
820
821                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
822                 $struct->joiner( '&' );
823             }
824         } 
825
826         last unless ($_);
827
828     }
829
830     $struct = undef if 
831         scalar(@{$struct->query_nodes}) == 0 &&
832         scalar(@{$struct->filters}) == 0 &&
833         !$struct->top_plan;
834
835     return $struct if !wantarray;
836     return ($struct, $remainder);
837 }
838
839 sub find_class_index {
840     my $class = shift;
841     my $query = shift;
842
843     my ($class_part, @field_parts) = split '\|', $class;
844     $class_part ||= $class;
845
846     for my $idx ( 0 .. scalar(@$query) - 1 ) {
847         next unless ref($$query[$idx]);
848         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
849     }
850
851     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
852     return -1;
853 }
854
855 sub core_limit {
856     my $self = shift;
857     my $l = shift;
858     $self->{core_limit} = $l if ($l);
859     return $self->{core_limit};
860 }
861
862 sub superpage {
863     my $self = shift;
864     my $l = shift;
865     $self->{superpage} = $l if ($l);
866     return $self->{superpage};
867 }
868
869 sub superpage_size {
870     my $self = shift;
871     my $l = shift;
872     $self->{superpage_size} = $l if ($l);
873     return $self->{superpage_size};
874 }
875
876
877 #-------------------------------
878 package QueryParser::_util;
879
880 # At this level, joiners are always & or |.  This is not
881 # the external, configurable representation of joiners that
882 # defaults to # && and ||.
883 sub is_joiner {
884     my $str = shift;
885
886     return (not ref $str and ($str eq '&' or $str eq '|'));
887 }
888
889 sub default_joiner { '&' }
890
891 # 0 for different, 1 for the same.
892 sub compare_abstract_atoms {
893     my ($left, $right) = @_;
894
895     foreach (qw/prefix suffix content/) {
896         no warnings;    # undef can stand in for '' here
897         return 0 unless $left->{$_} eq $right->{$_};
898     }
899
900     return 1;
901 }
902
903 sub fake_abstract_atom_from_phrase {
904     my $phrase = shift;
905     my $neg = shift;
906     my $qp_class = shift || 'QueryParser';
907
908     my $prefix = '"';
909     if ($neg) {
910         $prefix =
911             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
912             $prefix;
913     }
914
915     return {
916         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
917         "content" => $phrase
918     }
919 }
920
921 sub find_arrays_in_abstract {
922     my ($hash) = @_;
923
924     my @arrays;
925     foreach my $key (keys %$hash) {
926         if (ref $hash->{$key} eq "ARRAY") {
927             push @arrays, $hash->{$key};
928             foreach (@{$hash->{$key}}) {
929                 push @arrays, find_arrays_in_abstract($_);
930             }
931         }
932     }
933
934     return @arrays;
935 }
936
937 #-------------------------------
938 package QueryParser::Canonicalize;  # not OO
939
940 sub _abstract_query2str_filter {
941     my $f = shift;
942     my $qp_class = shift || 'QueryParser';
943     my $qpconfig = $QueryParser::parser_config{$qp_class};
944
945     return sprintf(
946         '%s%s(%s)',
947         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
948         $f->{name},
949         join(",", @{$f->{args}})
950     );
951 }
952
953 sub _abstract_query2str_modifier {
954     my $f = shift;
955     my $qp_class = shift || 'QueryParser';
956     my $qpconfig = $QueryParser::parser_config{$qp_class};
957
958     return $qpconfig->{operators}{modifier} . $f;
959 }
960
961 sub _kid_list {
962     my $children = shift;
963     my $op = (keys %$children)[0];
964     return @{$$children{$op}};
965 }
966
967 # This should produce an equivalent query to the original, given an
968 # abstract_query.
969 sub abstract_query2str_impl {
970     my $abstract_query  = shift;
971     my $depth = shift || 0;
972
973     my $qp_class ||= shift || 'QueryParser';
974     my $qpconfig = $QueryParser::parser_config{$qp_class};
975
976     my $fs = $qpconfig->{operators}{float_start};
977     my $fe = $qpconfig->{operators}{float_end};
978     my $gs = $qpconfig->{operators}{group_start};
979     my $ge = $qpconfig->{operators}{group_end};
980     my $and = $qpconfig->{operators}{and};
981     my $or = $qpconfig->{operators}{or};
982
983     my $isnode = 0;
984     my $q = "";
985
986     if (exists $abstract_query->{type}) {
987         if ($abstract_query->{type} eq 'query_plan') {
988             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
989                 exists $abstract_query->{filters};
990
991             $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
992                 exists $abstract_query->{modifiers};
993             $isnode = 1
994                 if (!$abstract_query->{floating} && exists $abstract_query->{children} && _kid_list($abstract_query->{children}) > 1);
995         } elsif ($abstract_query->{type} eq 'node') {
996             if ($abstract_query->{alias}) {
997                 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
998                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
999             } else {
1000                 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1001                 $q .= "|$_" foreach @{$abstract_query->{fields}};
1002             }
1003             $q .= ":";
1004             $isnode = 1;
1005         } elsif ($abstract_query->{type} eq 'atom') {
1006             my $prefix = $abstract_query->{prefix} || '';
1007             $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
1008             $q .= ($q ? ' ' : '') . $prefix .
1009                 ($abstract_query->{content} || '') .
1010                 ($abstract_query->{suffix} || '');
1011         } elsif ($abstract_query->{type} eq 'facet') {
1012             # facet syntax [ # ] is hardcoded I guess?
1013             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1014             $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1015                 join(" # ", @{$abstract_query->{values}}) . "]";
1016         }
1017     }
1018
1019     if (exists $abstract_query->{children}) {
1020
1021         my $op = (keys(%{$abstract_query->{children}}))[0];
1022
1023         if ($abstract_query->{floating}) { # always the top node!
1024             my $sub_node = pop @{$abstract_query->{children}{$op}};
1025
1026             $abstract_query->{floating} = 0;
1027             $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class) . $fe. " ";
1028
1029             $abstract_query = $sub_node;
1030         }
1031
1032         if ($abstract_query && exists $abstract_query->{children}) {
1033             $op = (keys(%{$abstract_query->{children}}))[0];
1034             $q .= ($q ? ' ' : '') . join(
1035                 ($op eq '&' ? ' ' : " $or "),
1036                 map {
1037                     my $x = abstract_query2str_impl($_, $depth + 1, $qp_class); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1038                 } @{$abstract_query->{children}{$op}}
1039             );
1040         }
1041     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1042         my $op = (keys(%{$abstract_query}))[0];
1043         $q .= ($q ? ' ' : '') . join(
1044             ($op eq '&' ? ' ' : " $or "),
1045             map {
1046                     my $x = abstract_query2str_impl($_, $depth + 1, $qp_class); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1047             } @{$abstract_query->{$op}}
1048         );
1049     }
1050
1051     $q = "$gs$q$ge" if ($isnode);
1052
1053     return $q;
1054 }
1055
1056 #-------------------------------
1057 package QueryParser::query_plan;
1058
1059 sub QueryParser {
1060     my $self = shift;
1061     return undef unless ref($self);
1062     return $self->{QueryParser};
1063 }
1064
1065 sub new {
1066     my $pkg = shift;
1067     $pkg = ref($pkg) || $pkg;
1068     my %args = (query => [], joiner => '&', @_);
1069
1070     return bless \%args => $pkg;
1071 }
1072
1073 sub new_node {
1074     my $self = shift;
1075     my $pkg = ref($self) || $self;
1076     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1077     $self->add_node( $node );
1078     return $node;
1079 }
1080
1081 sub new_facet {
1082     my $self = shift;
1083     my $pkg = ref($self) || $self;
1084     my $name = shift;
1085     my $args = shift;
1086     my $negate = shift;
1087
1088     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1089     $self->add_node( $node );
1090
1091     return $node;
1092 }
1093
1094 sub new_filter {
1095     my $self = shift;
1096     my $pkg = ref($self) || $self;
1097     my $name = shift;
1098     my $args = shift;
1099     my $negate = shift;
1100
1101     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1102     $self->add_filter( $node );
1103
1104     return $node;
1105 }
1106
1107
1108 sub _merge_filters {
1109     my $left_filter = shift;
1110     my $right_filter = shift;
1111     my $join = shift;
1112
1113     return undef unless $left_filter or $right_filter;
1114     return $right_filter unless $left_filter;
1115     return $left_filter unless $right_filter;
1116
1117     my $args = $left_filter->{args} || [];
1118
1119     if ($join eq '|') {
1120         push(@$args, @{$right_filter->{args}});
1121
1122     } else {
1123         # find the intersect values
1124         my %new_vals;
1125         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1126         $args = [ grep { $new_vals{$_} } @$args ];
1127     }
1128
1129     $left_filter->{args} = $args;
1130     return $left_filter;
1131 }
1132
1133 sub collapse_filters {
1134     my $self = shift;
1135     my $name = shift;
1136
1137     # start by merging any filters at this level.
1138     # like-level filters are always ORed together
1139
1140     my $cur_filter;
1141     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1142     if (@cur_filters) {
1143         $cur_filter = shift @cur_filters;
1144         my $args = $cur_filter->{args} || [];
1145         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1146     }
1147
1148     # next gather the collapsed filters from sub-plans and 
1149     # merge them with our own
1150
1151     my @subquery = @{$self->{query}};
1152
1153     while (@subquery) {
1154         my $blob = shift @subquery;
1155         shift @subquery; # joiner
1156         next unless $blob->isa('QueryParser::query_plan');
1157         my $sub_filter = $blob->collapse_filters($name);
1158         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1159     }
1160
1161     if ($self->QueryParser->debug) {
1162         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1163         warn "collapse_filters($name) => [@args]\n";
1164     }
1165
1166     return $cur_filter;
1167 }
1168
1169 sub find_filter {
1170     my $self = shift;
1171     my $needle = shift;;
1172     return undef unless ($needle);
1173
1174     my $filter = $self->collapse_filters($needle);
1175
1176     warn "find_filter($needle) => " . 
1177         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1178         if $self->QueryParser->debug;
1179
1180     return $filter ? ($filter) : ();
1181 }
1182
1183 sub find_modifier {
1184     my $self = shift;
1185     my $needle = shift;;
1186     return undef unless ($needle);
1187     return grep { $_->name eq $needle } @{ $self->modifiers };
1188 }
1189
1190 sub new_modifier {
1191     my $self = shift;
1192     my $pkg = ref($self) || $self;
1193     my $name = shift;
1194
1195     my $node = do{$pkg.'::modifier'}->new( $name );
1196     $self->add_modifier( $node );
1197
1198     return $node;
1199 }
1200
1201 sub classed_node {
1202     my $self = shift;
1203     my $requested_class = shift;
1204
1205     my $node;
1206     for my $n (@{$self->{query}}) {
1207         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1208         if ($n->requested_class eq $requested_class) {
1209             $node = $n;
1210             last;
1211         }
1212     }
1213
1214     if (!$node) {
1215         $node = $self->new_node;
1216         $node->requested_class( $requested_class );
1217     }
1218
1219     return $node;
1220 }
1221
1222 sub remove_last_node {
1223     my $self = shift;
1224     my $requested_class = shift;
1225
1226     my $old = pop(@{$self->query_nodes});
1227     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1228
1229     return $old;
1230 }
1231
1232 sub query_nodes {
1233     my $self = shift;
1234     return $self->{query};
1235 }
1236
1237 sub floating {
1238     my $self = shift;
1239     my $f = shift;
1240     $self->{floating} = $f if (defined $f);
1241     return $self->{floating};
1242 }
1243
1244 sub add_node {
1245     my $self = shift;
1246     my $node = shift;
1247
1248     $self->{query} ||= [];
1249     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1250     push(@{$self->{query}}, $node);
1251
1252     return $self;
1253 }
1254
1255 sub top_plan {
1256     my $self = shift;
1257
1258     return $self->{level} ? 0 : 1;
1259 }
1260
1261 sub plan_level {
1262     my $self = shift;
1263     return $self->{level};
1264 }
1265
1266 sub joiner {
1267     my $self = shift;
1268     my $joiner = shift;
1269
1270     $self->{joiner} = $joiner if ($joiner);
1271     return $self->{joiner};
1272 }
1273
1274 sub modifiers {
1275     my $self = shift;
1276     $self->{modifiers} ||= [];
1277     return $self->{modifiers};
1278 }
1279
1280 sub add_modifier {
1281     my $self = shift;
1282     my $modifier = shift;
1283
1284     $self->{modifiers} ||= [];
1285     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1286
1287     push(@{$self->{modifiers}}, $modifier);
1288
1289     return $self;
1290 }
1291
1292 sub facets {
1293     my $self = shift;
1294     $self->{facets} ||= [];
1295     return $self->{facets};
1296 }
1297
1298 sub add_facet {
1299     my $self = shift;
1300     my $facet = shift;
1301
1302     $self->{facets} ||= [];
1303     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1304
1305     push(@{$self->{facets}}, $facet);
1306
1307     return $self;
1308 }
1309
1310 sub filters {
1311     my $self = shift;
1312     $self->{filters} ||= [];
1313     return $self->{filters};
1314 }
1315
1316 sub add_filter {
1317     my $self = shift;
1318     my $filter = shift;
1319
1320     $self->{filters} ||= [];
1321
1322     push(@{$self->{filters}}, $filter);
1323
1324     return $self;
1325 }
1326
1327 # %opts supports two options at this time:
1328 #   no_phrases :
1329 #       If true, do not do anything to the phrases and unphrases
1330 #       fields on any discovered nodes.
1331 #   with_config :
1332 #       If true, also return the query parser config as part of the blob.
1333 #       This will get set back to 0 before recursion to avoid repetition.
1334 sub to_abstract_query {
1335     my $self = shift;
1336     my %opts = @_;
1337
1338     my $pkg = ref $self->QueryParser || $self->QueryParser;
1339
1340     my $abstract_query = {
1341         type => "query_plan",
1342         floating => $self->floating,
1343         filters => [map { $_->to_abstract_query } @{$self->filters}],
1344         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1345     };
1346
1347     if ($opts{with_config}) {
1348         $opts{with_config} = 0;
1349         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1350     }
1351
1352     my $kids = [];
1353
1354     for my $qnode (@{$self->query_nodes}) {
1355         # Remember: qnode can be a joiner string, a node, or another query_plan
1356
1357         if (QueryParser::_util::is_joiner($qnode)) {
1358             if ($abstract_query->{children}) {
1359                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1360                 next if $open_joiner eq $qnode;
1361
1362                 my $oldroot = $abstract_query->{children};
1363                 $kids = [$oldroot];
1364                 $abstract_query->{children} = {$qnode => $kids};
1365             } else {
1366                 $abstract_query->{children} = {$qnode => $kids};
1367             }
1368         } else {
1369             push @$kids, $qnode->to_abstract_query(%opts);
1370         }
1371     }
1372
1373     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1374     return $abstract_query;
1375 }
1376
1377
1378 #-------------------------------
1379 package QueryParser::query_plan::node;
1380 use Data::Dumper;
1381 $Data::Dumper::Indent = 0;
1382
1383 sub new {
1384     my $pkg = shift;
1385     $pkg = ref($pkg) || $pkg;
1386     my %args = @_;
1387
1388     return bless \%args => $pkg;
1389 }
1390
1391 sub new_atom {
1392     my $self = shift;
1393     my $pkg = ref($self) || $self;
1394     return do{$pkg.'::atom'}->new( @_ );
1395 }
1396
1397 sub requested_class { # also split into classname, fields and alias
1398     my $self = shift;
1399     my $class = shift;
1400
1401     if ($class) {
1402         my @afields;
1403         my (undef, $alias) = split '#', $class;
1404         if ($alias) {
1405             $class =~ s/#[^|]+//;
1406             ($alias, @afields) = split '\|', $alias;
1407         }
1408
1409         my @fields = @afields;
1410         my ($class_part, @field_parts) = split '\|', $class;
1411         for my $f (@field_parts) {
1412              push(@fields, $f) unless (grep { $f eq $_ } @fields);
1413         }
1414
1415         $class_part ||= $class;
1416
1417         $self->{requested_class} = $class;
1418         $self->{alias} = $alias if $alias;
1419         $self->{alias_fields} = \@afields if $alias;
1420         $self->{classname} = $class_part;
1421         $self->{fields} = \@fields;
1422     }
1423
1424     return $self->{requested_class};
1425 }
1426
1427 sub plan {
1428     my $self = shift;
1429     my $plan = shift;
1430
1431     $self->{plan} = $plan if ($plan);
1432     return $self->{plan};
1433 }
1434
1435 sub alias {
1436     my $self = shift;
1437     my $alias = shift;
1438
1439     $self->{alias} = $alias if ($alias);
1440     return $self->{alias};
1441 }
1442
1443 sub alias_fields {
1444     my $self = shift;
1445     my $alias = shift;
1446
1447     $self->{alias_fields} = $alias if ($alias);
1448     return $self->{alias_fields};
1449 }
1450
1451 sub classname {
1452     my $self = shift;
1453     my $class = shift;
1454
1455     $self->{classname} = $class if ($class);
1456     return $self->{classname};
1457 }
1458
1459 sub fields {
1460     my $self = shift;
1461     my @fields = @_;
1462
1463     $self->{fields} ||= [];
1464     $self->{fields} = \@fields if (@fields);
1465     return $self->{fields};
1466 }
1467
1468 sub phrases {
1469     my $self = shift;
1470     my @phrases = @_;
1471
1472     $self->{phrases} ||= [];
1473     $self->{phrases} = \@phrases if (@phrases);
1474     return $self->{phrases};
1475 }
1476
1477 sub unphrases {
1478     my $self = shift;
1479     my @phrases = @_;
1480
1481     $self->{unphrases} ||= [];
1482     $self->{unphrases} = \@phrases if (@phrases);
1483     return $self->{unphrases};
1484 }
1485
1486 sub add_phrase {
1487     my $self = shift;
1488     my $phrase = shift;
1489
1490     push(@{$self->phrases}, $phrase);
1491
1492     return $self;
1493 }
1494
1495 sub add_unphrase {
1496     my $self = shift;
1497     my $phrase = shift;
1498
1499     push(@{$self->unphrases}, $phrase);
1500
1501     return $self;
1502 }
1503
1504 sub query_atoms {
1505     my $self = shift;
1506     my @query_atoms = @_;
1507
1508     $self->{query_atoms} ||= [];
1509     $self->{query_atoms} = \@query_atoms if (@query_atoms);
1510     return $self->{query_atoms};
1511 }
1512
1513 sub add_fts_atom {
1514     my $self = shift;
1515     my $atom = shift;
1516
1517     if (!ref($atom)) {
1518         my $content = $atom;
1519         my @parts = @_;
1520
1521         $atom = $self->new_atom( content => $content, @parts );
1522     }
1523
1524     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1525     push(@{$self->query_atoms}, $atom);
1526
1527     return $self;
1528 }
1529
1530 sub add_dummy_atom {
1531     my $self = shift;
1532     my @parts = @_;
1533
1534     my $atom = $self->new_atom( @parts, dummy => 1 );
1535
1536     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1537     push(@{$self->query_atoms}, $atom);
1538
1539     return $self;
1540 }
1541
1542 # This will find up to one occurence of @$short_list within @$long_list, and
1543 # replace it with the single atom $replacement.
1544 sub replace_phrase_in_abstract_query {
1545     my ($self, $short_list, $long_list, $replacement) = @_;
1546
1547     my $success = 0;
1548     my @already = ();
1549     my $goal = scalar @$short_list;
1550
1551     for (my $i = 0; $i < scalar (@$long_list); $i++) {
1552         my $right = $long_list->[$i];
1553
1554         if (QueryParser::_util::compare_abstract_atoms(
1555             $short_list->[scalar @already], $right
1556         )) {
1557             push @already, $i;
1558         } elsif (scalar @already) {
1559             @already = ();
1560             next;
1561         }
1562
1563         if (scalar @already == $goal) {
1564             splice @$long_list, $already[0], scalar(@already), $replacement;
1565             $success = 1;
1566             last;
1567         }
1568     }
1569
1570     return $success;
1571 }
1572
1573 sub to_abstract_query {
1574     my $self = shift;
1575     my %opts = @_;
1576
1577     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
1578
1579     my $abstract_query = {
1580         "type" => "node",
1581         "alias" => $self->alias,
1582         "alias_fields" => $self->alias_fields,
1583         "class" => $self->classname,
1584         "fields" => $self->fields
1585     };
1586
1587     my $kids = [];
1588
1589     for my $qatom (@{$self->query_atoms}) {
1590         if (QueryParser::_util::is_joiner($qatom)) {
1591             if ($abstract_query->{children}) {
1592                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1593                 next if $open_joiner eq $qatom;
1594
1595                 my $oldroot = $abstract_query->{children};
1596                 $kids = [$oldroot];
1597                 $abstract_query->{children} = {$qatom => $kids};
1598             } else {
1599                 $abstract_query->{children} = {$qatom => $kids};
1600             }
1601         } else {
1602             push @$kids, $qatom->to_abstract_query;
1603         }
1604     }
1605
1606     if ($self->{phrases} and not $opts{no_phrases}) {
1607         for my $phrase (@{$self->{phrases}}) {
1608             # Phrases appear duplication in a real QP tree, and we don't want
1609             # that duplication in our abstract query.  So for all our phrases,
1610             # break them into atoms as QP would, and remove any matching
1611             # sequences of atoms from our abstract query.
1612
1613             my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
1614             if ($tmptree) {
1615                 # For a well-behaved phrase, we should now have only one node
1616                 # in the $tmptree query plan, and that node should have an
1617                 # orderly list of atoms and joiners.
1618
1619                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1620                     my $tmplist;
1621
1622                     eval {
1623                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1624                             no_phrases => 1
1625                         )->{children}->{'&'}->[0]->{children}->{'&'};
1626                     };
1627                     next if $@;
1628
1629                     foreach (
1630                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1631                     ) {
1632                         last if $self->replace_phrase_in_abstract_query(
1633                             $tmplist,
1634                             $_,
1635                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
1636                         );
1637                     }
1638                 }
1639             }
1640         }
1641     }
1642
1643     # Do the same as the preceding block for unphrases (negated phrases).
1644     if ($self->{unphrases} and not $opts{no_phrases}) {
1645         for my $phrase (@{$self->{unphrases}}) {
1646             my $tmptree = $self->{plan}->{QueryParser}->new(
1647                 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
1648                     '"' . $phrase . '"'
1649             )->parse->parse_tree;
1650
1651             if ($tmptree) {
1652                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1653                     my $tmplist;
1654
1655                     eval {
1656                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1657                             no_phrases => 1
1658                         )->{children}->{'&'}->[0]->{children}->{'&'};
1659                     };
1660                     next if $@;
1661
1662                     foreach (
1663                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1664                     ) {
1665                         last if $self->replace_phrase_in_abstract_query(
1666                             $tmplist,
1667                             $_,
1668                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
1669                         );
1670                     }
1671                 }
1672             }
1673         }
1674     }
1675
1676     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1677     return $abstract_query;
1678 }
1679
1680 #-------------------------------
1681 package QueryParser::query_plan::node::atom;
1682
1683 sub new {
1684     my $pkg = shift;
1685     $pkg = ref($pkg) || $pkg;
1686     my %args = @_;
1687
1688     return bless \%args => $pkg;
1689 }
1690
1691 sub node {
1692     my $self = shift;
1693     return undef unless (ref $self);
1694     return $self->{node};
1695 }
1696
1697 sub content {
1698     my $self = shift;
1699     return undef unless (ref $self);
1700     return $self->{content};
1701 }
1702
1703 sub prefix {
1704     my $self = shift;
1705     return undef unless (ref $self);
1706     return $self->{prefix};
1707 }
1708
1709 sub suffix {
1710     my $self = shift;
1711     return undef unless (ref $self);
1712     return $self->{suffix};
1713 }
1714
1715 sub to_abstract_query {
1716     my ($self) = @_;
1717     
1718     return {
1719         (map { $_ => $self->$_ } qw/prefix suffix content/),
1720         "type" => "atom"
1721     };
1722 }
1723 #-------------------------------
1724 package QueryParser::query_plan::filter;
1725
1726 sub new {
1727     my $pkg = shift;
1728     $pkg = ref($pkg) || $pkg;
1729     my %args = @_;
1730
1731     return bless \%args => $pkg;
1732 }
1733
1734 sub plan {
1735     my $self = shift;
1736     return $self->{plan};
1737 }
1738
1739 sub name {
1740     my $self = shift;
1741     return $self->{name};
1742 }
1743
1744 sub negate {
1745     my $self = shift;
1746     return $self->{negate};
1747 }
1748
1749 sub args {
1750     my $self = shift;
1751     return $self->{args};
1752 }
1753
1754 sub to_abstract_query {
1755     my ($self) = @_;
1756     
1757     return {
1758         map { $_ => $self->$_ } qw/name negate args/
1759     };
1760 }
1761
1762 #-------------------------------
1763 package QueryParser::query_plan::facet;
1764
1765 sub new {
1766     my $pkg = shift;
1767     $pkg = ref($pkg) || $pkg;
1768     my %args = @_;
1769
1770     return bless \%args => $pkg;
1771 }
1772
1773 sub plan {
1774     my $self = shift;
1775     return $self->{plan};
1776 }
1777
1778 sub name {
1779     my $self = shift;
1780     return $self->{name};
1781 }
1782
1783 sub negate {
1784     my $self = shift;
1785     return $self->{negate};
1786 }
1787
1788 sub values {
1789     my $self = shift;
1790     return $self->{'values'};
1791 }
1792
1793 sub to_abstract_query {
1794     my ($self) = @_;
1795
1796     return {
1797         (map { $_ => $self->$_ } qw/name negate values/),
1798         "type" => "facet"
1799     };
1800 }
1801
1802 #-------------------------------
1803 package QueryParser::query_plan::modifier;
1804
1805 sub new {
1806     my $pkg = shift;
1807     $pkg = ref($pkg) || $pkg;
1808     my $modifier = shift;
1809     my $negate = shift;
1810
1811     return bless { name => $modifier, negate => $negate } => $pkg;
1812 }
1813
1814 sub name {
1815     my $self = shift;
1816     return $self->{name};
1817 }
1818
1819 sub negate {
1820     my $self = shift;
1821     return $self->{negate};
1822 }
1823
1824 sub to_abstract_query {
1825     my ($self) = @_;
1826     
1827     return $self->name;
1828 }
1829 1;
1830