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