]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
Teach QP about floating (force-to-top) subplans indicated by {{...}}
[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) {
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) {
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 => '&' );
701             $struct->add_node($_) for ($LHS, $RHS);
702
703             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
704
705             $last_type = 'AND';
706         } elsif (/$or_re/) { # ORed expression
707             $_ = $';
708             next if ($last_type eq 'AND');
709             next if ($last_type eq 'OR');
710             warn "Encountered OR\n" if $self->debug;
711
712             my $LHS = $struct;
713             my ($RHS, $subremainder) = $self->decompose( $group_start.$_.$group_end, $current_class, $recursing + 1 );
714             $_ = $subremainder;
715
716             $struct = $self->new_plan( level => $recursing, joiner => '|' );
717             $struct->add_node($_) for ($LHS, $RHS);
718
719             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
720
721             $last_type = 'OR';
722         } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
723             warn "Encountered facet: $1$2 => $3\n" if $self->debug;
724
725             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
726             my $facet = $2;
727             my $facet_value = [ split '\s*#\s*', $3 ];
728             $struct->new_facet( $facet => $facet_value, $negate );
729             $_ = $';
730
731             $last_type = '';
732         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
733
734             if ($last_type eq 'CLASS') {
735                 $struct->remove_last_node( $current_class );
736                 warn "Encountered class change with no searches!\n" if $self->debug;
737             }
738
739             warn "Encountered class change: $1\n" if $self->debug;
740
741             $current_class = $struct->classed_node( $1 )->requested_class();
742             $_ = $';
743
744             $last_type = 'CLASS';
745         } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
746             warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
747
748             my $req_ness = $1 || '';
749             my $phrase = $2;
750
751             if (!$phrase_helper) {
752                 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
753                 my $after = $';
754                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
755                 $struct->add_node( $substruct ) if ($substruct);
756                 $_ = $after;
757             } else {
758                 warn "Directly parsing the phrase subquery\n" if $self->debug;
759                 $struct->joiner( '&' );
760
761                 my $class_node = $struct->classed_node($current_class);
762
763                 if ($req_ness eq $pkg->operator('disallowed')) {
764                     $class_node->add_dummy_atom( node => $class_node );
765                     $class_node->add_unphrase( $phrase );
766                     $phrase = '';
767                     #$phrase =~ s/(^|\s)\b/$1-/g;
768                 } else { 
769                     $class_node->add_phrase( $phrase );
770                 }
771                 $_ = $phrase . $';
772
773             }
774
775             $last_type = '';
776
777 #        } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
778 #            warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
779 #
780 #            my $phrase = $1;
781 #
782 #            my $class_node = $struct->classed_node($current_class);
783 #            $class_node->add_phrase( $phrase );
784 #            $_ = $phrase . $';
785 #            $struct->joiner( '&' );
786 #
787 #            $last_type = '';
788         } elsif (/^\s*([^$group_end\s]+)/o && /^\s*([^$float_end\s]+)/o) { # atom
789             warn "Encountered atom: $1\n" if $self->debug;
790             warn "Remainder: $'\n" if $self->debug;
791
792             my $atom = $1;
793             my $after = $';
794
795             $_ = $after;
796             $last_type = '';
797
798             my $class_node = $struct->classed_node($current_class);
799
800             my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
801             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
802
803             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
804 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
805 #                $class_node->add_unphrase( $atom ) if ($prefix eq '!');
806
807                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
808                 $struct->joiner( '&' );
809             }
810         } 
811
812         last unless ($_);
813
814     }
815
816     $struct = undef if 
817         scalar(@{$struct->query_nodes}) == 0 &&
818         scalar(@{$struct->filters}) == 0 &&
819         !$struct->top_plan;
820
821     return $struct if !wantarray;
822     return ($struct, $remainder);
823 }
824
825 sub find_class_index {
826     my $class = shift;
827     my $query = shift;
828
829     my ($class_part, @field_parts) = split '\|', $class;
830     $class_part ||= $class;
831
832     for my $idx ( 0 .. scalar(@$query) - 1 ) {
833         next unless ref($$query[$idx]);
834         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
835     }
836
837     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
838     return -1;
839 }
840
841 sub core_limit {
842     my $self = shift;
843     my $l = shift;
844     $self->{core_limit} = $l if ($l);
845     return $self->{core_limit};
846 }
847
848 sub superpage {
849     my $self = shift;
850     my $l = shift;
851     $self->{superpage} = $l if ($l);
852     return $self->{superpage};
853 }
854
855 sub superpage_size {
856     my $self = shift;
857     my $l = shift;
858     $self->{superpage_size} = $l if ($l);
859     return $self->{superpage_size};
860 }
861
862
863 #-------------------------------
864 package QueryParser::_util;
865
866 # At this level, joiners are always & or |.  This is not
867 # the external, configurable representation of joiners that
868 # defaults to # && and ||.
869 sub is_joiner {
870     my $str = shift;
871
872     return (not ref $str and ($str eq '&' or $str eq '|'));
873 }
874
875 sub default_joiner { '&' }
876
877 # 0 for different, 1 for the same.
878 sub compare_abstract_atoms {
879     my ($left, $right) = @_;
880
881     foreach (qw/prefix suffix content/) {
882         no warnings;    # undef can stand in for '' here
883         return 0 unless $left->{$_} eq $right->{$_};
884     }
885
886     return 1;
887 }
888
889 sub fake_abstract_atom_from_phrase {
890     my $phrase = shift;
891     my $neg = shift;
892     my $qp_class = shift || 'QueryParser';
893
894     my $prefix = '"';
895     if ($neg) {
896         $prefix =
897             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
898             $prefix;
899     }
900
901     return {
902         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
903         "content" => $phrase
904     }
905 }
906
907 sub find_arrays_in_abstract {
908     my ($hash) = @_;
909
910     my @arrays;
911     foreach my $key (keys %$hash) {
912         if (ref $hash->{$key} eq "ARRAY") {
913             push @arrays, $hash->{$key};
914             foreach (@{$hash->{$key}}) {
915                 push @arrays, find_arrays_in_abstract($_);
916             }
917         }
918     }
919
920     return @arrays;
921 }
922
923 #-------------------------------
924 package QueryParser::Canonicalize;  # not OO
925
926 sub _abstract_query2str_filter {
927     my $f = shift;
928     my $qp_class = shift || 'QueryParser';
929     my $qpconfig = $QueryParser::parser_config{$qp_class};
930
931     return sprintf(
932         '%s%s(%s)',
933         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
934         $f->{name},
935         join(",", @{$f->{args}})
936     );
937 }
938
939 sub _abstract_query2str_modifier {
940     my $f = shift;
941     my $qp_class = shift || 'QueryParser';
942     my $qpconfig = $QueryParser::parser_config{$qp_class};
943
944     return $qpconfig->{operators}{modifier} . $f;
945 }
946
947 # This should produce an equivalent query to the original, given an
948 # abstract_query.
949 sub abstract_query2str_impl {
950     my $abstract_query  = shift;
951     my $depth = shift || 0;
952
953     my $qp_class ||= shift || 'QueryParser';
954     my $qpconfig = $QueryParser::parser_config{$qp_class};
955
956     my $fs = $qpconfig->{operators}{float_start};
957     my $fe = $qpconfig->{operators}{float_end};
958     my $gs = $qpconfig->{operators}{group_start};
959     my $ge = $qpconfig->{operators}{group_end};
960     my $and = $qpconfig->{operators}{and};
961     my $or = $qpconfig->{operators}{or};
962
963     my $needs_group = 0;
964     my $q = "";
965
966     if (exists $abstract_query->{type}) {
967         if ($abstract_query->{type} eq 'query_plan') {
968             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
969                 exists $abstract_query->{filters};
970             $needs_group += scalar(@{$abstract_query->{filters}}) if exists $abstract_query->{filters};
971
972             $q .= " ";
973
974             $q .= join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
975                 exists $abstract_query->{modifiers};
976             $needs_group += scalar(@{$abstract_query->{modifiers}}) if exists $abstract_query->{modifiers};
977         } elsif ($abstract_query->{type} eq 'node') {
978             if ($abstract_query->{alias}) {
979                 $q .= " " . $abstract_query->{alias};
980                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
981             } else {
982                 $q .= " " . $abstract_query->{class};
983                 $q .= "|$_" foreach @{$abstract_query->{fields}};
984             }
985             $q .= ":";
986         } elsif ($abstract_query->{type} eq 'atom') {
987             my $prefix = $abstract_query->{prefix} || '';
988             $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
989             $q .= $prefix .
990                 ($abstract_query->{content} || '') .
991                 ($abstract_query->{suffix} || '');
992             $needs_group += 1;
993         } elsif ($abstract_query->{type} eq 'facet') {
994             # facet syntax [ # ] is hardcoded I guess?
995             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
996             $q .= $prefix . $abstract_query->{name} . "[" .
997                 join(" # ", @{$abstract_query->{values}}) . "]";
998             $needs_group += 1;
999         }
1000     }
1001
1002     if (exists $abstract_query->{children}) {
1003
1004         my $op = (keys(%{$abstract_query->{children}}))[0];
1005
1006         if ($abstract_query->{floating}) { # always the top node!
1007             my $sub_node = pop @{$abstract_query->{children}{$op}};
1008
1009             $abstract_query->{floating} = 0;
1010             $q = $fs.abstract_query2str_impl($abstract_query,0,$qp_class).$fe;
1011
1012             $abstract_query = $sub_node;
1013         }
1014
1015         if ($abstract_query && exists $abstract_query->{children}) {
1016             $op = (keys(%{$abstract_query->{children}}))[0];
1017             $q .= join(
1018                 " " . ($op eq '&' ? '' : $or) . " ",
1019                 map {
1020                     abstract_query2str_impl($_, $depth + 1, $qp_class)
1021                 } @{$abstract_query->{children}{$op}}
1022             );
1023             $needs_group += scalar(@{$abstract_query->{children}{$op}});
1024         }
1025     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1026         my $op = (keys(%{$abstract_query}))[0];
1027         $q .= join(
1028             " " . ($op eq '&' ? '' : $or) . " ",
1029             map {
1030                 abstract_query2str_impl($_, $depth + 1, $qp_class)
1031             } @{$abstract_query->{$op}}
1032         );
1033         $needs_group += scalar(@{$abstract_query->{$op}});
1034     }
1035     $q .= " ";
1036
1037     $q = $gs . $q . $ge if ($needs_group > 1 and $depth);
1038
1039     return $q;
1040 }
1041
1042 #-------------------------------
1043 package QueryParser::query_plan;
1044
1045 sub QueryParser {
1046     my $self = shift;
1047     return undef unless ref($self);
1048     return $self->{QueryParser};
1049 }
1050
1051 sub new {
1052     my $pkg = shift;
1053     $pkg = ref($pkg) || $pkg;
1054     my %args = (query => [], joiner => '&', @_);
1055
1056     return bless \%args => $pkg;
1057 }
1058
1059 sub new_node {
1060     my $self = shift;
1061     my $pkg = ref($self) || $self;
1062     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1063     $self->add_node( $node );
1064     return $node;
1065 }
1066
1067 sub new_facet {
1068     my $self = shift;
1069     my $pkg = ref($self) || $self;
1070     my $name = shift;
1071     my $args = shift;
1072     my $negate = shift;
1073
1074     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1075     $self->add_node( $node );
1076
1077     return $node;
1078 }
1079
1080 sub new_filter {
1081     my $self = shift;
1082     my $pkg = ref($self) || $self;
1083     my $name = shift;
1084     my $args = shift;
1085     my $negate = shift;
1086
1087     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1088     $self->add_filter( $node );
1089
1090     return $node;
1091 }
1092
1093
1094 sub _merge_filters {
1095     my $left_filter = shift;
1096     my $right_filter = shift;
1097     my $join = shift;
1098
1099     return undef unless $left_filter or $right_filter;
1100     return $right_filter unless $left_filter;
1101     return $left_filter unless $right_filter;
1102
1103     my $args = $left_filter->{args} || [];
1104
1105     if ($join eq '|') {
1106         push(@$args, @{$right_filter->{args}});
1107
1108     } else {
1109         # find the intersect values
1110         my %new_vals;
1111         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1112         $args = [ grep { $new_vals{$_} } @$args ];
1113     }
1114
1115     $left_filter->{args} = $args;
1116     return $left_filter;
1117 }
1118
1119 sub collapse_filters {
1120     my $self = shift;
1121     my $name = shift;
1122
1123     # start by merging any filters at this level.
1124     # like-level filters are always ORed together
1125
1126     my $cur_filter;
1127     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1128     if (@cur_filters) {
1129         $cur_filter = shift @cur_filters;
1130         my $args = $cur_filter->{args} || [];
1131         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1132     }
1133
1134     # next gather the collapsed filters from sub-plans and 
1135     # merge them with our own
1136
1137     my @subquery = @{$self->{query}};
1138
1139     while (@subquery) {
1140         my $blob = shift @subquery;
1141         shift @subquery; # joiner
1142         next unless $blob->isa('QueryParser::query_plan');
1143         my $sub_filter = $blob->collapse_filters($name);
1144         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1145     }
1146
1147     if ($self->QueryParser->debug) {
1148         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1149         warn "collapse_filters($name) => [@args]\n";
1150     }
1151
1152     return $cur_filter;
1153 }
1154
1155 sub find_filter {
1156     my $self = shift;
1157     my $needle = shift;;
1158     return undef unless ($needle);
1159
1160     my $filter = $self->collapse_filters($needle);
1161
1162     warn "find_filter($needle) => " . 
1163         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1164         if $self->QueryParser->debug;
1165
1166     return $filter ? ($filter) : ();
1167 }
1168
1169 sub find_modifier {
1170     my $self = shift;
1171     my $needle = shift;;
1172     return undef unless ($needle);
1173     return grep { $_->name eq $needle } @{ $self->modifiers };
1174 }
1175
1176 sub new_modifier {
1177     my $self = shift;
1178     my $pkg = ref($self) || $self;
1179     my $name = shift;
1180
1181     my $node = do{$pkg.'::modifier'}->new( $name );
1182     $self->add_modifier( $node );
1183
1184     return $node;
1185 }
1186
1187 sub classed_node {
1188     my $self = shift;
1189     my $requested_class = shift;
1190
1191     my $node;
1192     for my $n (@{$self->{query}}) {
1193         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1194         if ($n->requested_class eq $requested_class) {
1195             $node = $n;
1196             last;
1197         }
1198     }
1199
1200     if (!$node) {
1201         $node = $self->new_node;
1202         $node->requested_class( $requested_class );
1203     }
1204
1205     return $node;
1206 }
1207
1208 sub remove_last_node {
1209     my $self = shift;
1210     my $requested_class = shift;
1211
1212     my $old = pop(@{$self->query_nodes});
1213     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1214
1215     return $old;
1216 }
1217
1218 sub query_nodes {
1219     my $self = shift;
1220     return $self->{query};
1221 }
1222
1223 sub add_node {
1224     my $self = shift;
1225     my $node = shift;
1226
1227     $self->{query} ||= [];
1228     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1229     push(@{$self->{query}}, $node);
1230
1231     return $self;
1232 }
1233
1234 sub top_plan {
1235     my $self = shift;
1236
1237     return $self->{level} ? 0 : 1;
1238 }
1239
1240 sub plan_level {
1241     my $self = shift;
1242     return $self->{level};
1243 }
1244
1245 sub joiner {
1246     my $self = shift;
1247     my $joiner = shift;
1248
1249     $self->{joiner} = $joiner if ($joiner);
1250     return $self->{joiner};
1251 }
1252
1253 sub modifiers {
1254     my $self = shift;
1255     $self->{modifiers} ||= [];
1256     return $self->{modifiers};
1257 }
1258
1259 sub add_modifier {
1260     my $self = shift;
1261     my $modifier = shift;
1262
1263     $self->{modifiers} ||= [];
1264     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1265
1266     push(@{$self->{modifiers}}, $modifier);
1267
1268     return $self;
1269 }
1270
1271 sub facets {
1272     my $self = shift;
1273     $self->{facets} ||= [];
1274     return $self->{facets};
1275 }
1276
1277 sub add_facet {
1278     my $self = shift;
1279     my $facet = shift;
1280
1281     $self->{facets} ||= [];
1282     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1283
1284     push(@{$self->{facets}}, $facet);
1285
1286     return $self;
1287 }
1288
1289 sub filters {
1290     my $self = shift;
1291     $self->{filters} ||= [];
1292     return $self->{filters};
1293 }
1294
1295 sub add_filter {
1296     my $self = shift;
1297     my $filter = shift;
1298
1299     $self->{filters} ||= [];
1300
1301     push(@{$self->{filters}}, $filter);
1302
1303     return $self;
1304 }
1305
1306 # %opts supports two options at this time:
1307 #   no_phrases :
1308 #       If true, do not do anything to the phrases and unphrases
1309 #       fields on any discovered nodes.
1310 #   with_config :
1311 #       If true, also return the query parser config as part of the blob.
1312 #       This will get set back to 0 before recursion to avoid repetition.
1313 sub to_abstract_query {
1314     my $self = shift;
1315     my %opts = @_;
1316
1317     my $pkg = ref $self->QueryParser || $self->QueryParser;
1318
1319     my $abstract_query = {
1320         type => "query_plan",
1321         floating => $self->{floating},
1322         filters => [map { $_->to_abstract_query } @{$self->filters}],
1323         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1324     };
1325
1326     if ($opts{with_config}) {
1327         $opts{with_config} = 0;
1328         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1329     }
1330
1331     my $kids = [];
1332
1333     for my $qnode (@{$self->query_nodes}) {
1334         # Remember: qnode can be a joiner string, a node, or another query_plan
1335
1336         if (QueryParser::_util::is_joiner($qnode)) {
1337             if ($abstract_query->{children}) {
1338                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1339                 next if $open_joiner eq $qnode;
1340
1341                 my $oldroot = $abstract_query->{children};
1342                 $kids = [$oldroot];
1343                 $abstract_query->{children} = {$qnode => $kids};
1344             } else {
1345                 $abstract_query->{children} = {$qnode => $kids};
1346             }
1347         } else {
1348             push @$kids, $qnode->to_abstract_query(%opts);
1349         }
1350     }
1351
1352     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1353     return $abstract_query;
1354 }
1355
1356
1357 #-------------------------------
1358 package QueryParser::query_plan::node;
1359 use Data::Dumper;
1360 $Data::Dumper::Indent = 0;
1361
1362 sub new {
1363     my $pkg = shift;
1364     $pkg = ref($pkg) || $pkg;
1365     my %args = @_;
1366
1367     return bless \%args => $pkg;
1368 }
1369
1370 sub new_atom {
1371     my $self = shift;
1372     my $pkg = ref($self) || $self;
1373     return do{$pkg.'::atom'}->new( @_ );
1374 }
1375
1376 sub requested_class { # also split into classname, fields and alias
1377     my $self = shift;
1378     my $class = shift;
1379
1380     if ($class) {
1381         my @afields;
1382         my (undef, $alias) = split '#', $class;
1383         if ($alias) {
1384             $class =~ s/#[^|]+//;
1385             ($alias, @afields) = split '\|', $alias;
1386         }
1387
1388         my @fields = @afields;
1389         my ($class_part, @field_parts) = split '\|', $class;
1390         for my $f (@field_parts) {
1391              push(@fields, $f) unless (grep { $f eq $_ } @fields);
1392         }
1393
1394         $class_part ||= $class;
1395
1396         $self->{requested_class} = $class;
1397         $self->{alias} = $alias if $alias;
1398         $self->{alias_fields} = \@afields if $alias;
1399         $self->{classname} = $class_part;
1400         $self->{fields} = \@fields;
1401     }
1402
1403     return $self->{requested_class};
1404 }
1405
1406 sub plan {
1407     my $self = shift;
1408     my $plan = shift;
1409
1410     $self->{plan} = $plan if ($plan);
1411     return $self->{plan};
1412 }
1413
1414 sub alias {
1415     my $self = shift;
1416     my $alias = shift;
1417
1418     $self->{alias} = $alias if ($alias);
1419     return $self->{alias};
1420 }
1421
1422 sub alias_fields {
1423     my $self = shift;
1424     my $alias = shift;
1425
1426     $self->{alias_fields} = $alias if ($alias);
1427     return $self->{alias_fields};
1428 }
1429
1430 sub classname {
1431     my $self = shift;
1432     my $class = shift;
1433
1434     $self->{classname} = $class if ($class);
1435     return $self->{classname};
1436 }
1437
1438 sub fields {
1439     my $self = shift;
1440     my @fields = @_;
1441
1442     $self->{fields} ||= [];
1443     $self->{fields} = \@fields if (@fields);
1444     return $self->{fields};
1445 }
1446
1447 sub phrases {
1448     my $self = shift;
1449     my @phrases = @_;
1450
1451     $self->{phrases} ||= [];
1452     $self->{phrases} = \@phrases if (@phrases);
1453     return $self->{phrases};
1454 }
1455
1456 sub unphrases {
1457     my $self = shift;
1458     my @phrases = @_;
1459
1460     $self->{unphrases} ||= [];
1461     $self->{unphrases} = \@phrases if (@phrases);
1462     return $self->{unphrases};
1463 }
1464
1465 sub add_phrase {
1466     my $self = shift;
1467     my $phrase = shift;
1468
1469     push(@{$self->phrases}, $phrase);
1470
1471     return $self;
1472 }
1473
1474 sub add_unphrase {
1475     my $self = shift;
1476     my $phrase = shift;
1477
1478     push(@{$self->unphrases}, $phrase);
1479
1480     return $self;
1481 }
1482
1483 sub query_atoms {
1484     my $self = shift;
1485     my @query_atoms = @_;
1486
1487     $self->{query_atoms} ||= [];
1488     $self->{query_atoms} = \@query_atoms if (@query_atoms);
1489     return $self->{query_atoms};
1490 }
1491
1492 sub add_fts_atom {
1493     my $self = shift;
1494     my $atom = shift;
1495
1496     if (!ref($atom)) {
1497         my $content = $atom;
1498         my @parts = @_;
1499
1500         $atom = $self->new_atom( content => $content, @parts );
1501     }
1502
1503     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1504     push(@{$self->query_atoms}, $atom);
1505
1506     return $self;
1507 }
1508
1509 sub add_dummy_atom {
1510     my $self = shift;
1511     my @parts = @_;
1512
1513     my $atom = $self->new_atom( @parts, dummy => 1 );
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 # This will find up to one occurence of @$short_list within @$long_list, and
1522 # replace it with the single atom $replacement.
1523 sub replace_phrase_in_abstract_query {
1524     my ($self, $short_list, $long_list, $replacement) = @_;
1525
1526     my $success = 0;
1527     my @already = ();
1528     my $goal = scalar @$short_list;
1529
1530     for (my $i = 0; $i < scalar (@$long_list); $i++) {
1531         my $right = $long_list->[$i];
1532
1533         if (QueryParser::_util::compare_abstract_atoms(
1534             $short_list->[scalar @already], $right
1535         )) {
1536             push @already, $i;
1537         } elsif (scalar @already) {
1538             @already = ();
1539             next;
1540         }
1541
1542         if (scalar @already == $goal) {
1543             splice @$long_list, $already[0], scalar(@already), $replacement;
1544             $success = 1;
1545             last;
1546         }
1547     }
1548
1549     return $success;
1550 }
1551
1552 sub to_abstract_query {
1553     my $self = shift;
1554     my %opts = @_;
1555
1556     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
1557
1558     my $abstract_query = {
1559         "type" => "node",
1560         "alias" => $self->alias,
1561         "alias_fields" => $self->alias_fields,
1562         "class" => $self->classname,
1563         "fields" => $self->fields
1564     };
1565
1566     my $kids = [];
1567
1568     for my $qatom (@{$self->query_atoms}) {
1569         if (QueryParser::_util::is_joiner($qatom)) {
1570             if ($abstract_query->{children}) {
1571                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1572                 next if $open_joiner eq $qatom;
1573
1574                 my $oldroot = $abstract_query->{children};
1575                 $kids = [$oldroot];
1576                 $abstract_query->{children} = {$qatom => $kids};
1577             } else {
1578                 $abstract_query->{children} = {$qatom => $kids};
1579             }
1580         } else {
1581             push @$kids, $qatom->to_abstract_query;
1582         }
1583     }
1584
1585     if ($self->{phrases} and not $opts{no_phrases}) {
1586         for my $phrase (@{$self->{phrases}}) {
1587             # Phrases appear duplication in a real QP tree, and we don't want
1588             # that duplication in our abstract query.  So for all our phrases,
1589             # break them into atoms as QP would, and remove any matching
1590             # sequences of atoms from our abstract query.
1591
1592             my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
1593             if ($tmptree) {
1594                 # For a well-behaved phrase, we should now have only one node
1595                 # in the $tmptree query plan, and that node should have an
1596                 # orderly list of atoms and joiners.
1597
1598                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1599                     my $tmplist;
1600
1601                     eval {
1602                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1603                             no_phrases => 1
1604                         )->{children}->{'&'}->[0]->{children}->{'&'};
1605                     };
1606                     next if $@;
1607
1608                     foreach (
1609                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1610                     ) {
1611                         last if $self->replace_phrase_in_abstract_query(
1612                             $tmplist,
1613                             $_,
1614                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
1615                         );
1616                     }
1617                 }
1618             }
1619         }
1620     }
1621
1622     # Do the same as the preceding block for unphrases (negated phrases).
1623     if ($self->{unphrases} and not $opts{no_phrases}) {
1624         for my $phrase (@{$self->{unphrases}}) {
1625             my $tmptree = $self->{plan}->{QueryParser}->new(
1626                 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
1627                     '"' . $phrase . '"'
1628             )->parse->parse_tree;
1629
1630             if ($tmptree) {
1631                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1632                     my $tmplist;
1633
1634                     eval {
1635                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1636                             no_phrases => 1
1637                         )->{children}->{'&'}->[0]->{children}->{'&'};
1638                     };
1639                     next if $@;
1640
1641                     foreach (
1642                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1643                     ) {
1644                         last if $self->replace_phrase_in_abstract_query(
1645                             $tmplist,
1646                             $_,
1647                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
1648                         );
1649                     }
1650                 }
1651             }
1652         }
1653     }
1654
1655     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1656     return $abstract_query;
1657 }
1658
1659 #-------------------------------
1660 package QueryParser::query_plan::node::atom;
1661
1662 sub new {
1663     my $pkg = shift;
1664     $pkg = ref($pkg) || $pkg;
1665     my %args = @_;
1666
1667     return bless \%args => $pkg;
1668 }
1669
1670 sub node {
1671     my $self = shift;
1672     return undef unless (ref $self);
1673     return $self->{node};
1674 }
1675
1676 sub content {
1677     my $self = shift;
1678     return undef unless (ref $self);
1679     return $self->{content};
1680 }
1681
1682 sub prefix {
1683     my $self = shift;
1684     return undef unless (ref $self);
1685     return $self->{prefix};
1686 }
1687
1688 sub suffix {
1689     my $self = shift;
1690     return undef unless (ref $self);
1691     return $self->{suffix};
1692 }
1693
1694 sub to_abstract_query {
1695     my ($self) = @_;
1696     
1697     return {
1698         (map { $_ => $self->$_ } qw/prefix suffix content/),
1699         "type" => "atom"
1700     };
1701 }
1702 #-------------------------------
1703 package QueryParser::query_plan::filter;
1704
1705 sub new {
1706     my $pkg = shift;
1707     $pkg = ref($pkg) || $pkg;
1708     my %args = @_;
1709
1710     return bless \%args => $pkg;
1711 }
1712
1713 sub plan {
1714     my $self = shift;
1715     return $self->{plan};
1716 }
1717
1718 sub name {
1719     my $self = shift;
1720     return $self->{name};
1721 }
1722
1723 sub negate {
1724     my $self = shift;
1725     return $self->{negate};
1726 }
1727
1728 sub args {
1729     my $self = shift;
1730     return $self->{args};
1731 }
1732
1733 sub to_abstract_query {
1734     my ($self) = @_;
1735     
1736     return {
1737         map { $_ => $self->$_ } qw/name negate args/
1738     };
1739 }
1740
1741 #-------------------------------
1742 package QueryParser::query_plan::facet;
1743
1744 sub new {
1745     my $pkg = shift;
1746     $pkg = ref($pkg) || $pkg;
1747     my %args = @_;
1748
1749     return bless \%args => $pkg;
1750 }
1751
1752 sub plan {
1753     my $self = shift;
1754     return $self->{plan};
1755 }
1756
1757 sub name {
1758     my $self = shift;
1759     return $self->{name};
1760 }
1761
1762 sub negate {
1763     my $self = shift;
1764     return $self->{negate};
1765 }
1766
1767 sub values {
1768     my $self = shift;
1769     return $self->{'values'};
1770 }
1771
1772 sub to_abstract_query {
1773     my ($self) = @_;
1774
1775     return {
1776         (map { $_ => $self->$_ } qw/name negate values/),
1777         "type" => "facet"
1778     };
1779 }
1780
1781 #-------------------------------
1782 package QueryParser::query_plan::modifier;
1783
1784 sub new {
1785     my $pkg = shift;
1786     $pkg = ref($pkg) || $pkg;
1787     my $modifier = shift;
1788     my $negate = shift;
1789
1790     return bless { name => $modifier, negate => $negate } => $pkg;
1791 }
1792
1793 sub name {
1794     my $self = shift;
1795     return $self->{name};
1796 }
1797
1798 sub negate {
1799     my $self = shift;
1800     return $self->{negate};
1801 }
1802
1803 sub to_abstract_query {
1804     my ($self) = @_;
1805     
1806     return $self->name;
1807 }
1808 1;
1809